diff --git a/cura/BuildVolume.py b/cura/BuildVolume.py index b929f0f8f1..275fce6995 100755 --- a/cura/BuildVolume.py +++ b/cura/BuildVolume.py @@ -111,6 +111,9 @@ class BuildVolume(SceneNode): # but it does not update the disallowed areas after material change Application.getInstance().getMachineManager().activeStackChanged.connect(self._onStackChanged) + # Enable and disable extruder + Application.getInstance().getMachineManager().extruderChanged.connect(self.updateNodeBoundaryCheck) + # list of settings which were updated self._changed_settings_since_last_rebuild = [] @@ -217,30 +220,26 @@ class BuildVolume(SceneNode): group_nodes.append(node) # Keep list of affected group_nodes if node.callDecoration("isSliceable") or node.callDecoration("isGroup"): - node._outside_buildarea = False - bbox = node.getBoundingBox() - - # Mark the node as outside the build volume if the bounding box test fails. - if build_volume_bounding_box.intersectsBox(bbox) != AxisAlignedBox.IntersectionResult.FullIntersection: - node._outside_buildarea = True + if node.collidesWithBbox(build_volume_bounding_box): + node.setOutsideBuildArea(True) continue - convex_hull = node.callDecoration("getConvexHull") - if convex_hull: - if not convex_hull.isValid(): - return - # Check for collisions between disallowed areas and the object - for area in self.getDisallowedAreas(): - overlap = convex_hull.intersectsPolygon(area) - if overlap is None: - continue - node._outside_buildarea = True - continue + if node.collidesWithArea(self.getDisallowedAreas()): + node.setOutsideBuildArea(True) + continue + + # Mark the node as outside build volume if the set extruder is disabled + extruder_position = node.callDecoration("getActiveExtruderPosition") + if not self._global_container_stack.extruders[extruder_position].isEnabled: + node.setOutsideBuildArea(True) + continue + + node.setOutsideBuildArea(False) # 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 + child_node.setOutsideBuildArea(group_node.isOutsideBuildArea) ## Update the outsideBuildArea of a single node, given bounds or current build volume def checkBoundsAndUpdate(self, node: CuraSceneNode, bounds: Optional[AxisAlignedBox] = None): @@ -260,24 +259,20 @@ class BuildVolume(SceneNode): build_volume_bounding_box = bounds if node.callDecoration("isSliceable") or node.callDecoration("isGroup"): - bbox = node.getBoundingBox() - - # Mark the node as outside the build volume if the bounding box test fails. - if build_volume_bounding_box.intersectsBox(bbox) != AxisAlignedBox.IntersectionResult.FullIntersection: + if node.collidesWithBbox(build_volume_bounding_box): + node.setOutsideBuildArea(True) + return + + if node.collidesWithArea(self.getDisallowedAreas()): + node.setOutsideBuildArea(True) + return + + # Mark the node as outside build volume if the set extruder is disabled + extruder_position = node.callDecoration("getActiveExtruderPosition") + if not self._global_container_stack.extruders[extruder_position].isEnabled: node.setOutsideBuildArea(True) return - convex_hull = self.callDecoration("getConvexHull") - if convex_hull: - if not convex_hull.isValid(): - return - # Check for collisions between disallowed areas and the object - for area in self.getDisallowedAreas(): - overlap = convex_hull.intersectsPolygon(area) - if overlap is None: - continue - node.setOutsideBuildArea(True) - return node.setOutsideBuildArea(False) ## Recalculates the build volume & disallowed areas. diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index 5f07cf5e00..f146838069 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -1605,6 +1605,8 @@ class CuraApplication(QtApplication): fixed_nodes.append(node_) arranger = Arrange.create(fixed_nodes = fixed_nodes) min_offset = 8 + default_extruder_position = self.getMachineManager().defaultExtruderPosition + default_extruder_id = self._global_container_stack.extruders[default_extruder_position].getId() for original_node in nodes: @@ -1670,6 +1672,8 @@ class CuraApplication(QtApplication): op = AddSceneNodeOperation(node, scene.getRoot()) op.push() + + node.callDecoration("setActiveExtruder", default_extruder_id) scene.sceneChanged.emit(node) self.fileCompleted.emit(filename) diff --git a/cura/Machines/MaterialGroup.py b/cura/Machines/MaterialGroup.py index 75ab51182c..93c8a227a8 100644 --- a/cura/Machines/MaterialGroup.py +++ b/cura/Machines/MaterialGroup.py @@ -1,9 +1,10 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. +from typing import List +from cura.Machines.MaterialNode import MaterialNode #For type checking. -# -# A MaterialGroup represents a group of material InstanceContainers that are derived from a single material profile. +## A MaterialGroup represents a group of material InstanceContainers that are derived from a single material profile. # The main InstanceContainer which has the ID of the material profile file name is called the "root_material". For # example: "generic_abs" is the root material (ID) of "generic_abs_ultimaker3" and "generic_abs_ultimaker3_AA_0.4", # and "generic_abs_ultimaker3" and "generic_abs_ultimaker3_AA_0.4" are derived materials of "generic_abs". @@ -17,11 +18,11 @@ class MaterialGroup: __slots__ = ("name", "is_read_only", "root_material_node", "derived_material_node_list") - def __init__(self, name: str): + def __init__(self, name: str, root_material_node: MaterialNode): self.name = name self.is_read_only = False - self.root_material_node = None - self.derived_material_node_list = [] + self.root_material_node = root_material_node + self.derived_material_node_list = [] #type: List[MaterialNode] def __str__(self) -> str: return "%s[%s]" % (self.__class__.__name__, self.name) diff --git a/cura/Machines/MaterialManager.py b/cura/Machines/MaterialManager.py index d599676401..61b8bcd8e6 100644 --- a/cura/Machines/MaterialManager.py +++ b/cura/Machines/MaterialManager.py @@ -72,30 +72,28 @@ class MaterialManager(QObject): def initialize(self): # Find all materials and put them in a matrix for quick search. - material_metadata_list = self._container_registry.findContainersMetadata(type = "material") + material_metadatas = {metadata["id"]: metadata for metadata in self._container_registry.findContainersMetadata(type = "material")} self._material_group_map = dict() # Map #1 # root_material_id -> MaterialGroup - for material_metadata in material_metadata_list: - material_id = material_metadata["id"] + for material_id, material_metadata in material_metadatas.items(): # We don't store empty material in the lookup tables if material_id == "empty_material": continue root_material_id = material_metadata.get("base_file") if root_material_id not in self._material_group_map: - self._material_group_map[root_material_id] = MaterialGroup(root_material_id) + self._material_group_map[root_material_id] = MaterialGroup(root_material_id, MaterialNode(material_metadatas[root_material_id])) self._material_group_map[root_material_id].is_read_only = self._container_registry.isReadOnly(root_material_id) group = self._material_group_map[root_material_id] - # We only add root materials here - if material_id == root_material_id: - group.root_material_node = MaterialNode(material_metadata) - else: + #Store this material in the group of the appropriate root material. + if material_id != root_material_id: new_node = MaterialNode(material_metadata) group.derived_material_node_list.append(new_node) + # Order this map alphabetically so it's easier to navigate in a debugger self._material_group_map = OrderedDict(sorted(self._material_group_map.items(), key = lambda x: x[0])) @@ -180,7 +178,7 @@ class MaterialManager(QObject): # "machine" -> "variant_name" -> "root material ID" -> specific material InstanceContainer # Construct the "machine" -> "variant" -> "root material ID" -> specific material InstanceContainer self._diameter_machine_variant_material_map = dict() - for material_metadata in material_metadata_list: + for material_metadata in material_metadatas.values(): # We don't store empty material in the lookup tables if material_metadata["id"] == "empty_material": continue diff --git a/cura/Machines/Models/BrandMaterialsModel.py b/cura/Machines/Models/BrandMaterialsModel.py index 1146a8acee..e36c6448d3 100644 --- a/cura/Machines/Models/BrandMaterialsModel.py +++ b/cura/Machines/Models/BrandMaterialsModel.py @@ -4,8 +4,8 @@ from PyQt5.QtCore import Qt, pyqtSignal, pyqtProperty from UM.Qt.ListModel import ListModel - -from .BaseMaterialsModel import BaseMaterialsModel +from UM.Logger import Logger +from cura.Machines.Models.BaseMaterialsModel import BaseMaterialsModel # @@ -54,9 +54,7 @@ class BrandMaterialsModel(ListModel): self._material_manager = CuraApplication.getInstance().getMaterialManager() self._machine_manager.globalContainerChanged.connect(self._update) - self._extruder_manager.activeExtruderChanged.connect(self._update) self._material_manager.materialsUpdated.connect(self._update) - self._update() def setExtruderPosition(self, position: int): @@ -69,6 +67,7 @@ class BrandMaterialsModel(ListModel): return self._extruder_position def _update(self): + Logger.log("d", "Updating {model_class_name}.".format(model_class_name = self.__class__.__name__)) global_stack = self._machine_manager.activeMachine if global_stack is None: self.setItems([]) diff --git a/cura/Machines/Models/BuildPlateModel.py b/cura/Machines/Models/BuildPlateModel.py index 1cb94216a6..e1b4f40d8e 100644 --- a/cura/Machines/Models/BuildPlateModel.py +++ b/cura/Machines/Models/BuildPlateModel.py @@ -4,6 +4,7 @@ from PyQt5.QtCore import Qt from UM.Application import Application +from UM.Logger import Logger from UM.Qt.ListModel import ListModel from UM.Util import parseBool @@ -29,6 +30,7 @@ class BuildPlateModel(ListModel): self._update() def _update(self): + Logger.log("d", "Updating {model_class_name}.".format(model_class_name = self.__class__.__name__)) global_stack = self._machine_manager._global_container_stack if not global_stack: self.setItems([]) diff --git a/cura/Machines/Models/CustomQualityProfilesDropDownMenuModel.py b/cura/Machines/Models/CustomQualityProfilesDropDownMenuModel.py index a188a43e72..dcade8cb0d 100644 --- a/cura/Machines/Models/CustomQualityProfilesDropDownMenuModel.py +++ b/cura/Machines/Models/CustomQualityProfilesDropDownMenuModel.py @@ -12,7 +12,7 @@ from cura.Machines.Models.QualityProfilesDropDownMenuModel import QualityProfile class CustomQualityProfilesDropDownMenuModel(QualityProfilesDropDownMenuModel): def _update(self): - Logger.log("d", "Updating %s ...", self.__class__.__name__) + Logger.log("d", "Updating {model_class_name}.".format(model_class_name = self.__class__.__name__)) active_global_stack = self._machine_manager.activeMachine if active_global_stack is None: diff --git a/cura/Machines/Models/GenericMaterialsModel.py b/cura/Machines/Models/GenericMaterialsModel.py index 47240ebffd..6b149448ea 100644 --- a/cura/Machines/Models/GenericMaterialsModel.py +++ b/cura/Machines/Models/GenericMaterialsModel.py @@ -1,7 +1,8 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -from .BaseMaterialsModel import BaseMaterialsModel +from UM.Logger import Logger +from cura.Machines.Models.BaseMaterialsModel import BaseMaterialsModel class GenericMaterialsModel(BaseMaterialsModel): @@ -15,12 +16,12 @@ class GenericMaterialsModel(BaseMaterialsModel): self._material_manager = CuraApplication.getInstance().getMaterialManager() self._machine_manager.globalContainerChanged.connect(self._update) - self._extruder_manager.activeExtruderChanged.connect(self._update) self._material_manager.materialsUpdated.connect(self._update) - self._update() def _update(self): + Logger.log("d", "Updating {model_class_name}.".format(model_class_name = self.__class__.__name__)) + global_stack = self._machine_manager.activeMachine if global_stack is None: self.setItems([]) diff --git a/cura/Machines/Models/MaterialManagementModel.py b/cura/Machines/Models/MaterialManagementModel.py index 1ea0fd9cf7..46e9cb887a 100644 --- a/cura/Machines/Models/MaterialManagementModel.py +++ b/cura/Machines/Models/MaterialManagementModel.py @@ -1,8 +1,9 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -from PyQt5.QtCore import Qt, pyqtProperty +from PyQt5.QtCore import Qt +from UM.Logger import Logger from UM.Qt.ListModel import ListModel @@ -60,6 +61,8 @@ class MaterialManagementModel(ListModel): self._update() def _update(self): + Logger.log("d", "Updating {model_class_name}.".format(model_class_name = self.__class__.__name__)) + global_stack = self._machine_manager.activeMachine if global_stack is None: self.setItems([]) diff --git a/cura/Machines/Models/NozzleModel.py b/cura/Machines/Models/NozzleModel.py index 27d190962b..0879998b7d 100644 --- a/cura/Machines/Models/NozzleModel.py +++ b/cura/Machines/Models/NozzleModel.py @@ -4,6 +4,7 @@ from PyQt5.QtCore import Qt from UM.Application import Application +from UM.Logger import Logger from UM.Qt.ListModel import ListModel from UM.Util import parseBool @@ -20,26 +21,30 @@ class NozzleModel(ListModel): self.addRoleName(self.HotendNameRole, "hotend_name") self.addRoleName(self.ContainerNodeRole, "container_node") - Application.getInstance().globalContainerStackChanged.connect(self._update) - Application.getInstance().getMachineManager().activeVariantChanged.connect(self._update) - Application.getInstance().getMachineManager().activeStackChanged.connect(self._update) - Application.getInstance().getMachineManager().activeMaterialChanged.connect(self._update) + self._application = Application.getInstance() + self._machine_manager = self._application.getMachineManager() + self._variant_manager = self._application.getVariantManager() + + self._machine_manager.globalContainerChanged.connect(self._update) + self._update() def _update(self): + Logger.log("d", "Updating {model_class_name}.".format(model_class_name = self.__class__.__name__)) + self.items.clear() - variant_manager = Application.getInstance()._variant_manager - active_global_stack = Application.getInstance().getMachineManager()._global_container_stack - if active_global_stack is None: + global_stack = self._machine_manager.activeMachine + if global_stack is None: self.setItems([]) return - has_variants = parseBool(active_global_stack.getMetaDataEntry("has_variants", False)) + has_variants = parseBool(global_stack.getMetaDataEntry("has_variants", False)) if not has_variants: self.setItems([]) return - variant_node_dict = variant_manager.getVariantNodes(active_global_stack) + from cura.Machines.VariantManager import VariantType + variant_node_dict = self._variant_manager.getVariantNodes(global_stack, VariantType.NOZZLE) if not variant_node_dict: self.setItems([]) return diff --git a/cura/Machines/Models/QualityManagementModel.py b/cura/Machines/Models/QualityManagementModel.py index 542016ab68..4d2b551805 100644 --- a/cura/Machines/Models/QualityManagementModel.py +++ b/cura/Machines/Models/QualityManagementModel.py @@ -4,7 +4,7 @@ from PyQt5.QtCore import Qt, pyqtSlot from UM.Qt.ListModel import ListModel - +from UM.Logger import Logger # # This the QML model for the quality management page. @@ -35,6 +35,8 @@ class QualityManagementModel(ListModel): self._update() def _update(self): + Logger.log("d", "Updating {model_class_name}.".format(model_class_name = self.__class__.__name__)) + global_stack = self._machine_manager.activeMachine quality_group_dict = self._quality_manager.getQualityGroups(global_stack) diff --git a/cura/Machines/Models/QualityProfilesDropDownMenuModel.py b/cura/Machines/Models/QualityProfilesDropDownMenuModel.py index fd919639c3..d8c4b668cf 100644 --- a/cura/Machines/Models/QualityProfilesDropDownMenuModel.py +++ b/cura/Machines/Models/QualityProfilesDropDownMenuModel.py @@ -29,7 +29,7 @@ class QualityProfilesDropDownMenuModel(ListModel): self.addRoleName(self.QualityTypeRole, "quality_type") self.addRoleName(self.LayerHeightRole, "layer_height") self.addRoleName(self.LayerHeightUnitRole, "layer_height_unit") - self.addRoleName(self.AvailableRole, "available") + self.addRoleName(self.AvailableRole, "available") #Whether the quality profile is available in our current nozzle + material. self.addRoleName(self.QualityGroupRole, "quality_group") self.addRoleName(self.QualityChangesGroupRole, "quality_changes_group") @@ -39,6 +39,7 @@ class QualityProfilesDropDownMenuModel(ListModel): self._application.globalContainerStackChanged.connect(self._update) self._machine_manager.activeQualityGroupChanged.connect(self._update) + self._machine_manager.extruderChanged.connect(self._update) self._quality_manager.qualitiesUpdated.connect(self._update) self._layer_height_unit = "" # This is cached @@ -46,7 +47,7 @@ class QualityProfilesDropDownMenuModel(ListModel): self._update() def _update(self): - Logger.log("d", "Updating quality profile model ...") + Logger.log("d", "Updating {model_class_name}.".format(model_class_name = self.__class__.__name__)) global_stack = self._machine_manager.activeMachine if global_stack is None: diff --git a/cura/Machines/Models/QualitySettingsModel.py b/cura/Machines/Models/QualitySettingsModel.py index e0b29d77a5..4470ffc80e 100644 --- a/cura/Machines/Models/QualitySettingsModel.py +++ b/cura/Machines/Models/QualitySettingsModel.py @@ -21,6 +21,8 @@ class QualitySettingsModel(ListModel): UserValueRole = Qt.UserRole + 6 CategoryRole = Qt.UserRole + 7 + GLOBAL_STACK_POSITION = -1 + def __init__(self, parent = None): super().__init__(parent = parent) @@ -36,8 +38,7 @@ class QualitySettingsModel(ListModel): self._application = Application.getInstance() self._quality_manager = self._application.getQualityManager() - self._selected_position = "" # empty string means GlobalStack - # strings such as "0", "1", etc. mean extruder positions + self._selected_position = self.GLOBAL_STACK_POSITION #Must be either GLOBAL_STACK_POSITION or an extruder position (0, 1, etc.) self._selected_quality_item = None # The selected quality in the quality management page self._i18n_catalog = None @@ -54,7 +55,7 @@ class QualitySettingsModel(ListModel): self.selectedPositionChanged.emit() self._update() - @pyqtProperty(str, fset = setSelectedPosition, notify = selectedPositionChanged) + @pyqtProperty(int, fset = setSelectedPosition, notify = selectedPositionChanged) def selectedPosition(self): return self._selected_position @@ -69,6 +70,8 @@ class QualitySettingsModel(ListModel): return self._selected_quality_item def _update(self): + Logger.log("d", "Updating {model_class_name}.".format(model_class_name = self.__class__.__name__)) + if not self._selected_quality_item: self.setItems([]) return @@ -81,7 +84,7 @@ class QualitySettingsModel(ListModel): quality_group = self._selected_quality_item["quality_group"] quality_changes_group = self._selected_quality_item["quality_changes_group"] - if self._selected_position == "": + if self._selected_position == self.GLOBAL_STACK_POSITION: quality_node = quality_group.node_for_global else: quality_node = quality_group.nodes_for_extruders.get(self._selected_position) @@ -91,14 +94,14 @@ class QualitySettingsModel(ListModel): # Here, if the user has selected a quality changes, then "quality_changes_group" will not be None, and we fetch # the settings in that quality_changes_group. if quality_changes_group is not None: - if self._selected_position == "": + if self._selected_position == self.GLOBAL_STACK_POSITION: quality_changes_node = quality_changes_group.node_for_global else: quality_changes_node = quality_changes_group.nodes_for_extruders.get(self._selected_position) if quality_changes_node is not None: # it can be None if number of extruders are changed during runtime try: quality_containers.insert(0, quality_changes_node.getContainer()) - except: + except RuntimeError: # FIXME: This is to prevent incomplete update of QualityManager Logger.logException("d", "Failed to get container for quality changes node %s", quality_changes_node) return @@ -125,7 +128,7 @@ class QualitySettingsModel(ListModel): profile_value = new_value # Global tab should use resolve (if there is one) - if self._selected_position == "": + if self._selected_position == self.GLOBAL_STACK_POSITION: resolve_value = global_container_stack.getProperty(definition.key, "resolve") if resolve_value is not None and definition.key in settings_keys: profile_value = resolve_value @@ -133,10 +136,10 @@ class QualitySettingsModel(ListModel): if profile_value is not None: break - if not self._selected_position: + if self._selected_position == self.GLOBAL_STACK_POSITION: user_value = global_container_stack.userChanges.getProperty(definition.key, "value") else: - extruder_stack = global_container_stack.extruders[self._selected_position] + extruder_stack = global_container_stack.extruders[str(self._selected_position)] user_value = extruder_stack.userChanges.getProperty(definition.key, "value") if profile_value is None and user_value is None: diff --git a/cura/Machines/QualityGroup.py b/cura/Machines/QualityGroup.py index 6945162401..02096cfb36 100644 --- a/cura/Machines/QualityGroup.py +++ b/cura/Machines/QualityGroup.py @@ -1,7 +1,7 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -from typing import Optional, List +from typing import Dict, Optional, List from PyQt5.QtCore import QObject, pyqtSlot @@ -25,7 +25,7 @@ class QualityGroup(QObject): super().__init__(parent) self.name = name self.node_for_global = None # type: Optional["QualityGroup"] - self.nodes_for_extruders = dict() # position str -> QualityGroup + self.nodes_for_extruders = {} # type: Dict[int, "QualityGroup"] self.quality_type = quality_type self.is_available = False diff --git a/cura/Machines/QualityManager.py b/cura/Machines/QualityManager.py index ae7a177be4..efb940b857 100644 --- a/cura/Machines/QualityManager.py +++ b/cura/Machines/QualityManager.py @@ -159,9 +159,9 @@ class QualityManager(QObject): # Updates the given quality groups' availabilities according to which extruders are being used/ enabled. def _updateQualityGroupsAvailability(self, machine: "GlobalStack", quality_group_list): used_extruders = set() - # TODO: This will change after the Machine refactoring for i in range(machine.getProperty("machine_extruder_count", "value")): - used_extruders.add(str(i)) + if machine.extruders[str(i)].isEnabled: + used_extruders.add(str(i)) # Update the "is_available" flag for each quality group. for quality_group in quality_group_list: @@ -386,7 +386,7 @@ class QualityManager(QObject): if quality_changes_group is None: # create global quality changes only new_quality_changes = self._createQualityChanges(quality_group.quality_type, quality_changes_name, - global_stack, extruder_id = None) + global_stack, None) self._container_registry.addContainer(new_quality_changes) else: new_name = self._container_registry.uniqueName(quality_changes_name) diff --git a/cura/Machines/VariantManager.py b/cura/Machines/VariantManager.py index 196c2e7b1d..4e033e054e 100644 --- a/cura/Machines/VariantManager.py +++ b/cura/Machines/VariantManager.py @@ -104,11 +104,19 @@ class VariantManager: # Almost the same as getVariantMetadata() except that this returns an InstanceContainer if present. # def getVariantNode(self, machine_definition_id: str, variant_name: str, - variant_type: Optional["VariantType"] = VariantType.NOZZLE) -> Optional["ContainerNode"]: + variant_type: Optional["VariantType"] = None) -> Optional["ContainerNode"]: + if variant_type is None: + variant_node = None + variant_type_dict = self._machine_to_variant_dict_map[machine_definition_id] + for variant_dict in variant_type_dict.values(): + if variant_name in variant_dict: + variant_node = variant_dict[variant_name] + break + return variant_node return self._machine_to_variant_dict_map[machine_definition_id].get(variant_type, {}).get(variant_name) def getVariantNodes(self, machine: "GlobalStack", - variant_type: Optional["VariantType"] = VariantType.NOZZLE) -> dict: + variant_type: Optional["VariantType"] = None) -> dict: machine_definition_id = machine.definition.getId() return self._machine_to_variant_dict_map.get(machine_definition_id, {}).get(variant_type, {}) diff --git a/cura/Scene/CuraSceneNode.py b/cura/Scene/CuraSceneNode.py index df346baaad..b29108d636 100644 --- a/cura/Scene/CuraSceneNode.py +++ b/cura/Scene/CuraSceneNode.py @@ -1,9 +1,13 @@ +# Copyright (c) 2018 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. +from copy import deepcopy from typing import List from UM.Application import Application +from UM.Math.AxisAlignedBox import AxisAlignedBox from UM.Scene.SceneNode import SceneNode -from copy import deepcopy -from cura.Settings.ExtrudersModel import ExtrudersModel + +from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator ## Scene nodes that are models are only seen when selecting the corresponding build plate @@ -11,6 +15,8 @@ from cura.Settings.ExtrudersModel import ExtrudersModel class CuraSceneNode(SceneNode): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) + if "no_setting_override" not in kwargs: + self.addDecorator(SettingOverrideDecorator()) # now we always have a getActiveExtruderPosition, unless explicitly disabled self._outside_buildarea = False def setOutsideBuildArea(self, new_value): @@ -72,9 +78,34 @@ class CuraSceneNode(SceneNode): 1.0 ] + ## Return if the provided bbox collides with the bbox of this scene node + def collidesWithBbox(self, check_bbox): + bbox = self.getBoundingBox() + + # Mark the node as outside the build volume if the bounding box test fails. + if check_bbox.intersectsBox(bbox) != AxisAlignedBox.IntersectionResult.FullIntersection: + return True + + return False + + ## Return if any area collides with the convex hull of this scene node + def collidesWithArea(self, areas): + convex_hull = self.callDecoration("getConvexHull") + if convex_hull: + if not convex_hull.isValid(): + return False + + # Check for collisions between disallowed areas and the object + for area in areas: + overlap = convex_hull.intersectsPolygon(area) + if overlap is None: + continue + return True + return False + ## Taken from SceneNode, but replaced SceneNode with CuraSceneNode def __deepcopy__(self, memo): - copy = CuraSceneNode() + copy = CuraSceneNode(no_setting_override = True) # Setting override will be added later copy.setTransformation(self.getLocalTransformation()) copy.setMeshData(self._mesh_data) copy.setVisible(deepcopy(self._visible, memo)) diff --git a/cura/Settings/ContainerManager.py b/cura/Settings/ContainerManager.py index 345c25ded9..7161169b22 100644 --- a/cura/Settings/ContainerManager.py +++ b/cura/Settings/ContainerManager.py @@ -334,10 +334,13 @@ class ContainerManager(QObject): # Go through global and extruder stacks and clear their topmost container (the user settings). for stack in ExtruderManager.getInstance().getActiveGlobalAndExtruderStacks(): - container = stack.getTop() + container = stack.userChanges container.clear() send_emits_containers.append(container) + # user changes are possibly added to make the current setup match the current enabled extruders + Application.getInstance().getMachineManager().correctExtruderSettings() + for container in send_emits_containers: container.sendPostponedEmits() diff --git a/cura/Settings/CuraContainerRegistry.py b/cura/Settings/CuraContainerRegistry.py index 4b576a4207..81cbabc0c9 100644 --- a/cura/Settings/CuraContainerRegistry.py +++ b/cura/Settings/CuraContainerRegistry.py @@ -241,7 +241,7 @@ class CuraContainerRegistry(ContainerRegistry): profile.addMetaDataEntry("type", "quality_changes") profile.addMetaDataEntry("definition", global_profile.getMetaDataEntry("definition")) profile.addMetaDataEntry("quality_type", global_profile.getMetaDataEntry("quality_type")) - profile.addMetaDataEntry("extruder", extruder.getId()) + profile.addMetaDataEntry("position", "0") profile.setDirty(True) if idx == 0: # move all per-extruder settings to the first extruder's quality_changes @@ -273,10 +273,11 @@ class CuraContainerRegistry(ContainerRegistry): elif profile_index < len(machine_extruders) + 1: # This is assumed to be an extruder profile extruder_id = machine_extruders[profile_index - 1].definition.getId() - if not profile.getMetaDataEntry("extruder"): - profile.addMetaDataEntry("extruder", extruder_id) + extruder_position = str(profile_index - 1) + if not profile.getMetaDataEntry("position"): + profile.addMetaDataEntry("position", extruder_position) else: - profile.setMetaDataEntry("extruder", extruder_id) + profile.setMetaDataEntry("position", extruder_position) profile_id = (extruder_id + "_" + name_seed).lower().replace(" ", "_") else: #More extruders in the imported file than in the machine. diff --git a/cura/Settings/ExtruderManager.py b/cura/Settings/ExtruderManager.py index 06342b68e5..0ac3e4bd66 100755 --- a/cura/Settings/ExtruderManager.py +++ b/cura/Settings/ExtruderManager.py @@ -241,6 +241,13 @@ class ExtruderManager(QObject): result.append(extruder_stack.getProperty(setting_key, prop)) return result + def extruderValueWithDefault(self, value): + machine_manager = self._application.getMachineManager() + if value == "-1": + return machine_manager.defaultExtruderPosition + else: + return value + ## Gets the extruder stacks that are actually being used at the moment. # # An extruder stack is being used if it is the extruder to print any mesh @@ -252,7 +259,7 @@ class ExtruderManager(QObject): # # \return A list of extruder stacks. def getUsedExtruderStacks(self) -> List["ContainerStack"]: - global_stack = Application.getInstance().getGlobalContainerStack() + global_stack = self._application.getGlobalContainerStack() container_registry = ContainerRegistry.getInstance() used_extruder_stack_ids = set() @@ -302,16 +309,19 @@ class ExtruderManager(QObject): # Check support extruders if support_enabled: - used_extruder_stack_ids.add(self.extruderIds[str(global_stack.getProperty("support_infill_extruder_nr", "value"))]) - used_extruder_stack_ids.add(self.extruderIds[str(global_stack.getProperty("support_extruder_nr_layer_0", "value"))]) + used_extruder_stack_ids.add(self.extruderIds[self.extruderValueWithDefault(str(global_stack.getProperty("support_infill_extruder_nr", "value")))]) + used_extruder_stack_ids.add(self.extruderIds[self.extruderValueWithDefault(str(global_stack.getProperty("support_extruder_nr_layer_0", "value")))]) if support_bottom_enabled: - used_extruder_stack_ids.add(self.extruderIds[str(global_stack.getProperty("support_bottom_extruder_nr", "value"))]) + used_extruder_stack_ids.add(self.extruderIds[self.extruderValueWithDefault(str(global_stack.getProperty("support_bottom_extruder_nr", "value")))]) if support_roof_enabled: - used_extruder_stack_ids.add(self.extruderIds[str(global_stack.getProperty("support_roof_extruder_nr", "value"))]) + used_extruder_stack_ids.add(self.extruderIds[self.extruderValueWithDefault(str(global_stack.getProperty("support_roof_extruder_nr", "value")))]) # The platform adhesion extruder. Not used if using none. if global_stack.getProperty("adhesion_type", "value") != "none": - used_extruder_stack_ids.add(self.extruderIds[str(global_stack.getProperty("adhesion_extruder_nr", "value"))]) + extruder_nr = str(global_stack.getProperty("adhesion_extruder_nr", "value")) + if extruder_nr == "-1": + extruder_nr = Application.getInstance().getMachineManager().defaultExtruderPosition + used_extruder_stack_ids.add(self.extruderIds[extruder_nr]) try: return [container_registry.findContainerStacks(id = stack_id)[0] for stack_id in used_extruder_stack_ids] @@ -485,6 +495,8 @@ class ExtruderManager(QObject): result = [] for extruder in ExtruderManager.getInstance().getMachineExtruders(global_stack.getId()): + if not extruder.isEnabled: + continue # only include values from extruders that are "active" for the current machine instance if int(extruder.getMetaDataEntry("position")) >= global_stack.getProperty("machine_extruder_count", "value"): continue diff --git a/cura/Settings/ExtruderStack.py b/cura/Settings/ExtruderStack.py index 4400f621c6..5cdcba68b0 100644 --- a/cura/Settings/ExtruderStack.py +++ b/cura/Settings/ExtruderStack.py @@ -3,13 +3,14 @@ from typing import Any, TYPE_CHECKING, Optional -from PyQt5.QtCore import pyqtProperty +from PyQt5.QtCore import pyqtProperty, pyqtSignal from UM.Decorators import override from UM.MimeTypeDatabase import MimeType, MimeTypeDatabase from UM.Settings.ContainerStack import ContainerStack from UM.Settings.ContainerRegistry import ContainerRegistry from UM.Settings.Interfaces import ContainerInterface, PropertyEvaluationContext +from UM.Util import parseBool from . import Exceptions from .CuraContainerStack import CuraContainerStack, _ContainerIndexes @@ -30,6 +31,8 @@ class ExtruderStack(CuraContainerStack): self.propertiesChanged.connect(self._onPropertiesChanged) + enabledChanged = pyqtSignal() + ## Overridden from ContainerStack # # This will set the next stack and ensure that we register this stack as an extruder. @@ -46,6 +49,16 @@ class ExtruderStack(CuraContainerStack): def getNextStack(self) -> Optional["GlobalStack"]: return super().getNextStack() + def setEnabled(self, enabled): + if "enabled" not in self._metadata: + self.addMetaDataEntry("enabled", "True") + self.setMetaDataEntry("enabled", str(enabled)) + self.enabledChanged.emit() + + @pyqtProperty(bool, notify = enabledChanged) + def isEnabled(self): + return parseBool(self.getMetaDataEntry("enabled", "True")) + @classmethod def getLoadingPriority(cls) -> int: return 3 diff --git a/cura/Settings/ExtrudersModel.py b/cura/Settings/ExtrudersModel.py index 5139b9885d..4ee5ab3c3b 100644 --- a/cura/Settings/ExtrudersModel.py +++ b/cura/Settings/ExtrudersModel.py @@ -1,7 +1,7 @@ # Copyright (c) 2017 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -from PyQt5.QtCore import Qt, pyqtSignal, pyqtProperty, QTimer +from PyQt5.QtCore import Qt, pyqtSignal, pyqtSlot, pyqtProperty, QTimer from typing import Iterable from UM.i18n import i18nCatalog @@ -24,6 +24,8 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel): ## Human-readable name of the extruder. NameRole = Qt.UserRole + 2 + ## Is the extruder enabled? + EnabledRole = Qt.UserRole + 9 ## Colour of the material loaded in the extruder. ColorRole = Qt.UserRole + 3 @@ -43,6 +45,7 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel): # The variant of the extruder. VariantRole = Qt.UserRole + 7 + StackRole = Qt.UserRole + 8 ## List of colours to display if there is no material or the material has no known # colour. @@ -57,11 +60,13 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel): self.addRoleName(self.IdRole, "id") self.addRoleName(self.NameRole, "name") + self.addRoleName(self.EnabledRole, "enabled") self.addRoleName(self.ColorRole, "color") self.addRoleName(self.IndexRole, "index") self.addRoleName(self.DefinitionRole, "definition") self.addRoleName(self.MaterialRole, "material") self.addRoleName(self.VariantRole, "variant") + self.addRoleName(self.StackRole, "stack") self._update_extruder_timer = QTimer() self._update_extruder_timer.setInterval(100) @@ -183,11 +188,13 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel): item = { "id": extruder.getId(), "name": extruder.getName(), + "enabled": extruder.isEnabled, "color": color, "index": position, "definition": extruder.getBottom().getId(), "material": extruder.material.getName() if extruder.material else "", "variant": extruder.variant.getName() if extruder.variant else "", # e.g. print core + "stack": extruder, } items.append(item) diff --git a/cura/Settings/MachineManager.py b/cura/Settings/MachineManager.py index 7753dfced1..e8ee0b6657 100755 --- a/cura/Settings/MachineManager.py +++ b/cura/Settings/MachineManager.py @@ -54,6 +54,8 @@ class MachineManager(QObject): self._current_quality_group = None self._current_quality_changes_group = None + self._default_extruder_position = "0" # to be updated when extruders are switched on and off + self.machine_extruder_material_update_dict = collections.defaultdict(list) self._error_check_timer = QTimer() @@ -132,6 +134,7 @@ class MachineManager(QObject): # When the materials lookup table gets updated, it can mean that a material has its name changed, which should # be reflected on the GUI. This signal emission makes sure that it happens. self._material_manager.materialsUpdated.connect(self.rootMaterialChanged) + self.rootMaterialChanged.connect(self._onRootMaterialChanged) activeQualityGroupChanged = pyqtSignal() activeQualityChangesGroupChanged = pyqtSignal() @@ -141,6 +144,7 @@ class MachineManager(QObject): activeVariantChanged = pyqtSignal() activeQualityChanged = pyqtSignal() activeStackChanged = pyqtSignal() # Emitted whenever the active stack is changed (ie: when changing between extruders, changing a profile, but not when changing a value) + extruderChanged = pyqtSignal() globalValueChanged = pyqtSignal() # Emitted whenever a value inside global container is changed. activeStackValueChanged = pyqtSignal() # Emitted whenever a value inside the active stack is changed. @@ -232,7 +236,9 @@ class MachineManager(QObject): # Update the local global container stack reference self._global_container_stack = Application.getInstance().getGlobalContainerStack() - + if self._global_container_stack: + self.updateDefaultExtruder() + self.updateNumberExtrudersEnabled() self.globalContainerChanged.emit() # after switching the global stack we reconnect all the signals and set the variant and material references @@ -385,7 +391,7 @@ class MachineManager(QObject): return False if self._global_container_stack.hasErrors(): - Logger.log("d", "Checking global stack for errors took %0.2f s and we found and error" % (time.time() - time_start)) + Logger.log("d", "Checking global stack for errors took %0.2f s and we found an error" % (time.time() - time_start)) return True # Not a very pretty solution, but the extruder manager doesn't really know how many extruders there are @@ -715,6 +721,8 @@ class MachineManager(QObject): buildplate_compatible = True # It is compatible by default extruder_stacks = self._global_container_stack.extruders.values() for stack in extruder_stacks: + if not stack.isEnabled: + continue material_container = stack.material if material_container == self._empty_material_container: continue @@ -772,6 +780,43 @@ class MachineManager(QObject): if containers: return containers[0].definition.getId() + def getIncompatibleSettingsOnEnabledExtruders(self, container): + extruder_count = self._global_container_stack.getProperty("machine_extruder_count", "value") + result = [] + for setting_instance in container.findInstances(): + setting_key = setting_instance.definition.key + setting_enabled = self._global_container_stack.getProperty(setting_key, "enabled") + if not setting_enabled: + # A setting is not visible anymore + result.append(setting_key) + Logger.log("d", "Reset setting [%s] from [%s] because the setting is no longer enabled", setting_key, container) + continue + + if not self._global_container_stack.getProperty(setting_key, "type") in ("extruder", "optional_extruder"): + continue + + old_value = container.getProperty(setting_key, "value") + if int(old_value) >= extruder_count or not self._global_container_stack.extruders[str(old_value)].isEnabled: + result.append(setting_key) + Logger.log("d", "Reset setting [%s] in [%s] because its old value [%s] is no longer valid", setting_key, container, old_value) + return result + + ## Update extruder number to a valid value when the number of extruders are changed, or when an extruder is changed + def correctExtruderSettings(self): + for setting_key in self.getIncompatibleSettingsOnEnabledExtruders(self._global_container_stack.userChanges): + self._global_container_stack.userChanges.removeInstance(setting_key) + add_user_changes = self.getIncompatibleSettingsOnEnabledExtruders(self._global_container_stack.qualityChanges) + for setting_key in add_user_changes: + # Apply quality changes that are incompatible to user changes, so we do not change the quality changes itself. + self._global_container_stack.userChanges.setProperty(setting_key, "value", self._default_extruder_position) + if add_user_changes: + caution_message = Message(catalog.i18nc( + "@info:generic", + "Settings have been changed to match the current availability of extruders: [%s]" % ", ".join(add_user_changes)), + lifetime=0, + title = catalog.i18nc("@info:title", "Settings updated")) + caution_message.show() + ## Set the amount of extruders on the active machine (global stack) # \param extruder_count int the number of extruders to set def setActiveMachineExtruderCount(self, extruder_count): @@ -785,16 +830,11 @@ class MachineManager(QObject): if extruder_count == previous_extruder_count: return - # reset all extruder number settings whose value is no longer valid - for setting_instance in self._global_container_stack.userChanges.findInstances(): - setting_key = setting_instance.definition.key - if not self._global_container_stack.getProperty(setting_key, "type") in ("extruder", "optional_extruder"): - continue + definition_changes_container.setProperty("machine_extruder_count", "value", extruder_count) - old_value = int(self._global_container_stack.userChanges.getProperty(setting_key, "value")) - if old_value >= extruder_count: - self._global_container_stack.userChanges.removeInstance(setting_key) - Logger.log("d", "Reset [%s] because its old value [%s] is no longer valid ", setting_key, old_value) + self.updateDefaultExtruder() + self.updateNumberExtrudersEnabled() + self.correctExtruderSettings() # Check to see if any objects are set to print with an extruder that will no longer exist root_node = Application.getInstance().getController().getScene().getRoot() @@ -805,21 +845,19 @@ class MachineManager(QObject): if extruder_nr is not None and int(extruder_nr) > extruder_count - 1: node.callDecoration("setActiveExtruder", extruder_manager.getExtruderStack(extruder_count - 1).getId()) - definition_changes_container.setProperty("machine_extruder_count", "value", extruder_count) - # Make sure one of the extruder stacks is active extruder_manager.setActiveExtruderIndex(0) # Move settable_per_extruder values out of the global container # After CURA-4482 this should not be the case anymore, but we still want to support older project files. - global_user_container = self._global_container_stack.getTop() + global_user_container = self._global_container_stack.userChanges # Make sure extruder_stacks exists extruder_stacks = [] if previous_extruder_count == 1: extruder_stacks = ExtruderManager.getInstance().getActiveExtruderStacks() - global_user_container = self._global_container_stack.getTop() + global_user_container = self._global_container_stack.userChanges for setting_instance in global_user_container.findInstances(): setting_key = setting_instance.definition.key @@ -828,11 +866,12 @@ class MachineManager(QObject): if settable_per_extruder: limit_to_extruder = int(self._global_container_stack.getProperty(setting_key, "limit_to_extruder")) extruder_stack = extruder_stacks[max(0, limit_to_extruder)] - extruder_stack.getTop().setProperty(setting_key, "value", global_user_container.getProperty(setting_key, "value")) + extruder_stack.userChanges.setProperty(setting_key, "value", global_user_container.getProperty(setting_key, "value")) global_user_container.removeInstance(setting_key) # Signal that the global stack has changed Application.getInstance().globalContainerStackChanged.emit() + self.forceUpdateAllSettings() @pyqtSlot(int, result = QObject) def getExtruder(self, position: int): @@ -841,6 +880,53 @@ class MachineManager(QObject): extruder = self._global_container_stack.extruders.get(str(position)) return extruder + def updateDefaultExtruder(self): + extruder_items = sorted(self._global_container_stack.extruders.items()) + old_position = self._default_extruder_position + new_default_position = "0" + for position, extruder in extruder_items: + if extruder.isEnabled: + new_default_position = position + break + if new_default_position != old_position: + self._default_extruder_position = new_default_position + self.extruderChanged.emit() + + def updateNumberExtrudersEnabled(self): + definition_changes_container = self._global_container_stack.definitionChanges + extruder_count = 0 + for position, extruder in self._global_container_stack.extruders.items(): + if extruder.isEnabled: + extruder_count += 1 + definition_changes_container.setProperty("extruders_enabled_count", "value", extruder_count) + + @pyqtProperty(str, notify = extruderChanged) + def defaultExtruderPosition(self): + return self._default_extruder_position + + ## This will fire the propertiesChanged for all settings so they will be updated in the front-end + def forceUpdateAllSettings(self): + property_names = ["value", "resolve"] + for setting_key in self._global_container_stack.getAllKeys(): + self._global_container_stack.propertiesChanged.emit(setting_key, property_names) + + @pyqtSlot(int, bool) + def setExtruderEnabled(self, position: int, enabled) -> None: + extruder = self.getExtruder(position) + extruder.setEnabled(enabled) + self.updateDefaultExtruder() + self.updateNumberExtrudersEnabled() + self.correctExtruderSettings() + # ensure that the quality profile is compatible with current combination, or choose a compatible one if available + self._updateQualityWithMaterial() + self.extruderChanged.emit() + # update material compatibility color + self.activeQualityGroupChanged.emit() + # update items in SettingExtruder + ExtruderManager.getInstance().extrudersChanged.emit(self._global_container_stack.getId()) + # Make sure the front end reflects changes + self.forceUpdateAllSettings() + def _onMachineNameChanged(self): self.globalContainerChanged.emit() @@ -861,27 +947,32 @@ class MachineManager(QObject): container = extruder.userChanges container.setProperty(setting_name, property_name, property_value) - @pyqtProperty("QVariantList", notify = rootMaterialChanged) + @pyqtProperty("QVariantList", notify = globalContainerChanged) def currentExtruderPositions(self): - return sorted(list(self._current_root_material_id.keys())) + if self._global_container_stack is None: + return [] + return sorted(list(self._global_container_stack.extruders.keys())) - @pyqtProperty("QVariant", notify = rootMaterialChanged) - def currentRootMaterialId(self): - # initial filling the current_root_material_id + ## Update _current_root_material_id and _current_root_material_name when + # the current root material was changed. + def _onRootMaterialChanged(self): self._current_root_material_id = {} - for position in self._global_container_stack.extruders: - self._current_root_material_id[position] = self._global_container_stack.extruders[position].material.getMetaDataEntry("base_file") - return self._current_root_material_id - @pyqtProperty("QVariant", notify = rootMaterialChanged) - def currentRootMaterialName(self): - # initial filling the current_root_material_name if self._global_container_stack: + for position in self._global_container_stack.extruders: + self._current_root_material_id[position] = self._global_container_stack.extruders[position].material.getMetaDataEntry("base_file") self._current_root_material_name = {} for position in self._global_container_stack.extruders: if position not in self._current_root_material_name: material = self._global_container_stack.extruders[position].material self._current_root_material_name[position] = material.getName() + + @pyqtProperty("QVariant", notify = rootMaterialChanged) + def currentRootMaterialId(self): + return self._current_root_material_id + + @pyqtProperty("QVariant", notify = rootMaterialChanged) + def currentRootMaterialName(self): return self._current_root_material_name ## Return the variant names in the extruder stack(s). @@ -928,9 +1019,9 @@ class MachineManager(QObject): # Set quality and quality_changes for each ExtruderStack for position, node in quality_group.nodes_for_extruders.items(): - self._global_container_stack.extruders[position].quality = node.getContainer() + self._global_container_stack.extruders[str(position)].quality = node.getContainer() if empty_quality_changes: - self._global_container_stack.extruders[position].qualityChanges = self._empty_quality_changes_container + self._global_container_stack.extruders[str(position)].qualityChanges = self._empty_quality_changes_container self.activeQualityGroupChanged.emit() self.activeQualityChangesGroupChanged.emit() @@ -995,6 +1086,8 @@ class MachineManager(QObject): # check material - variant compatibility if Util.parseBool(self._global_container_stack.getMetaDataEntry("has_materials", False)): for position, extruder in self._global_container_stack.extruders.items(): + if extruder.isEnabled and not extruder.material.getMetaDataEntry("compatible"): + return False if not extruder.material.getMetaDataEntry("compatible"): return False return True diff --git a/cura/Settings/SettingOverrideDecorator.py b/cura/Settings/SettingOverrideDecorator.py index 24d94e4955..9054d9d04f 100644 --- a/cura/Settings/SettingOverrideDecorator.py +++ b/cura/Settings/SettingOverrideDecorator.py @@ -62,7 +62,7 @@ class SettingOverrideDecorator(SceneNodeDecorator): # use value from the stack because there can be a delay in signal triggering and "_is_non_printing_mesh" # has not been updated yet. - deep_copy._is_non_printing_mesh = any(bool(self._stack.getProperty(setting, "value")) for setting in self._non_printing_mesh_settings) + deep_copy._is_non_printing_mesh = self.evaluateIsNonPrintingMesh() return deep_copy @@ -90,10 +90,18 @@ class SettingOverrideDecorator(SceneNodeDecorator): def isNonPrintingMesh(self): return self._is_non_printing_mesh + def evaluateIsNonPrintingMesh(self): + return any(bool(self._stack.getProperty(setting, "value")) for setting in self._non_printing_mesh_settings) + def _onSettingChanged(self, instance, property_name): # Reminder: 'property' is a built-in function - # Trigger slice/need slicing if the value has changed. - if property_name == "value": - self._is_non_printing_mesh = any(bool(self._stack.getProperty(setting, "value")) for setting in self._non_printing_mesh_settings) + object_has_instance_setting = False + for container in self._stack.getContainers(): + if container.hasProperty(instance, "value"): + object_has_instance_setting = True + break + if property_name == "value" and object_has_instance_setting: + # Trigger slice/need slicing if the value has changed. + self._is_non_printing_mesh = self.evaluateIsNonPrintingMesh() Application.getInstance().getBackend().needsSlicing() Application.getInstance().getBackend().tickle() diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index 7201df65e4..3c627a7655 100755 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -216,11 +216,6 @@ class ThreeMFWorkspaceReader(WorkspaceReader): archive = zipfile.ZipFile(file_name, "r") cura_file_names = [name for name in archive.namelist() if name.startswith("Cura/")] - # A few lists of containers in this project files. - # When loading the global stack file, it may be associated with those containers, which may or may not be - # in Cura already, so we need to provide them as alternative search lists. - instance_container_list = [] - resolve_strategy_keys = ["machine", "material", "quality_changes"] self._resolve_strategies = {k: None for k in resolve_strategy_keys} containers_found_dict = {k: False for k in resolve_strategy_keys} @@ -307,34 +302,34 @@ class ThreeMFWorkspaceReader(WorkspaceReader): container_info = ContainerInfo(instance_container_file_name, serialized, parser) instance_container_info_dict[container_id] = container_info - instance_container = InstanceContainer(container_id) - - # Deserialize InstanceContainer by converting read data from bytes to string - instance_container.deserialize(serialized, file_name = instance_container_file_name) - instance_container_list.append(instance_container) - - container_type = instance_container.getMetaDataEntry("type") + container_type = parser["metadata"]["type"] if container_type == "quality_changes": quality_changes_info_list.append(container_info) - if not parser.has_option("metadata", "extruder"): + if not parser.has_option("metadata", "position"): self._machine_info.quality_changes_info.name = parser["general"]["name"] self._machine_info.quality_changes_info.global_info = container_info + else: + position = parser["metadata"]["position"] + self._machine_info.quality_changes_info.extruder_info_dict[position] = container_info - quality_name = instance_container.getName() - num_settings_overriden_by_quality_changes += len(instance_container._instances) + quality_name = parser["general"]["name"] + values = parser["values"] if parser.has_section("values") else dict() + num_settings_overriden_by_quality_changes += len(values) # Check if quality changes already exists. quality_changes = self._container_registry.findInstanceContainers(id = container_id) if quality_changes: containers_found_dict["quality_changes"] = True # Check if there really is a conflict by comparing the values + instance_container = InstanceContainer(container_id) + instance_container.deserialize(serialized, file_name = instance_container_file_name) if quality_changes[0] != instance_container: quality_changes_conflict = True elif container_type == "quality": if not quality_name: - quality_name = instance_container.getName() + quality_name = parser["general"]["name"] elif container_type == "user": - num_user_settings += len(instance_container._instances) + num_user_settings += len(parser["values"]) elif container_type in self._ignored_instance_container_types: # Ignore certain instance container types Logger.log("w", "Ignoring instance container [%s] with type [%s]", container_id, container_type) @@ -453,15 +448,6 @@ class ThreeMFWorkspaceReader(WorkspaceReader): machine_conflict = True break - if self._machine_info.quality_changes_info is not None: - for quality_changes_info in quality_changes_info_list: - if not quality_changes_info.parser.has_option("metadata", "extruder"): - continue - extruder_definition_id = quality_changes_info.parser["metadata"]["extruder"] - extruder_definition_metadata = self._container_registry.findDefinitionContainersMetadata(id = extruder_definition_id)[0] - position = extruder_definition_metadata["position"] - self._machine_info.quality_changes_info.extruder_info_dict[position] = quality_changes_info - num_visible_settings = 0 try: temp_preferences = Preferences() @@ -739,7 +725,6 @@ class ThreeMFWorkspaceReader(WorkspaceReader): from cura.Machines.QualityManager import getMachineDefinitionIDForQualitySearch machine_definition_id_for_quality = getMachineDefinitionIDForQualitySearch(global_stack) machine_definition_for_quality = self._container_registry.findDefinitionContainers(id = machine_definition_id_for_quality)[0] - extruder_dict_for_quality = machine_definition_for_quality.getMetaDataEntry("machine_extruder_trains") quality_changes_info = self._machine_info.quality_changes_info quality_changes_quality_type = quality_changes_info.global_info.parser["metadata"]["quality_type"] @@ -752,13 +737,12 @@ class ThreeMFWorkspaceReader(WorkspaceReader): quality_changes_name = self._container_registry.uniqueName(quality_changes_name) for position, container_info in container_info_dict.items(): - extruder_definition_id = None + extruder_stack = None if position is not None: - extruder_definition_id = extruder_dict_for_quality[position] - + extruder_stack = global_stack.extruders[position] container = quality_manager._createQualityChanges(quality_changes_quality_type, quality_changes_name, - global_stack, extruder_definition_id) + global_stack, extruder_stack) container_info.container = container container.setDirty(True) self._container_registry.addContainer(container) @@ -781,18 +765,14 @@ class ThreeMFWorkspaceReader(WorkspaceReader): container_info = quality_changes_info.extruder_info_dict[position] container_info.container = container - for position, container_info in quality_changes_info.extruder_info_dict.items(): - container_info.definition_id = extruder_dict_for_quality[position] - # If there is no quality changes for any extruder, create one. if not quality_changes_info.extruder_info_dict: container_info = ContainerInfo(None, None, None) quality_changes_info.extruder_info_dict["0"] = container_info - extruder_definition_id = extruder_dict_for_quality["0"] - container_info.definition_id = extruder_definition_id + extruder_stack = global_stack.extruders["0"] container = quality_manager._createQualityChanges(quality_changes_quality_type, quality_changes_name, - global_stack, extruder_definition_id) + global_stack, extruder_stack) container_info.container = container container.setDirty(True) self._container_registry.addContainer(container) @@ -818,9 +798,9 @@ class ThreeMFWorkspaceReader(WorkspaceReader): continue if container_info.container is None: - extruder_definition_id = extruder_dict_for_quality[position] + extruder_stack = global_stack.extruders[position] container = quality_manager._createQualityChanges(quality_changes_quality_type, quality_changes_name, - global_stack, extruder_definition_id) + global_stack, extruder_stack) container_info.container = container for key, value in container_info.parser["values"].items(): diff --git a/plugins/CuraEngineBackend/CuraEngineBackend.py b/plugins/CuraEngineBackend/CuraEngineBackend.py index ffeddf21cc..2f57e634e0 100755 --- a/plugins/CuraEngineBackend/CuraEngineBackend.py +++ b/plugins/CuraEngineBackend/CuraEngineBackend.py @@ -94,6 +94,8 @@ class CuraEngineBackend(QObject, Backend): self._onGlobalStackChanged() Application.getInstance().stacksValidationFinished.connect(self._onStackErrorCheckFinished) + # extruder enable / disable. Actually wanted to use machine manager here, but the initialization order causes it to crash + ExtruderManager.getInstance().extrudersChanged.connect(self._extruderChanged) # A flag indicating if an error check was scheduled # If so, we will stop the auto-slice timer and start upon the error check @@ -782,3 +784,9 @@ class CuraEngineBackend(QObject, Backend): def tickle(self): if self._use_timer: self._change_timer.start() + + def _extruderChanged(self): + for build_plate_number in range(Application.getInstance().getMultiBuildPlateModel().maxBuildPlate + 1): + if build_plate_number not in self._build_plates_to_be_sliced: + self._build_plates_to_be_sliced.append(build_plate_number) + self._invokeSlice() diff --git a/plugins/CuraEngineBackend/ProcessSlicedLayersJob.py b/plugins/CuraEngineBackend/ProcessSlicedLayersJob.py index c1fc597d80..cbc097bb33 100644 --- a/plugins/CuraEngineBackend/ProcessSlicedLayersJob.py +++ b/plugins/CuraEngineBackend/ProcessSlicedLayersJob.py @@ -81,7 +81,8 @@ class ProcessSlicedLayersJob(Job): Application.getInstance().getController().activeViewChanged.connect(self._onActiveViewChanged) - new_node = CuraSceneNode() + # The no_setting_override is here because adding the SettingOverrideDecorator will trigger a reslice + new_node = CuraSceneNode(no_setting_override = True) new_node.addDecorator(BuildPlateDecorator(self._build_plate_number)) # Force garbage collection. diff --git a/plugins/CuraEngineBackend/StartSliceJob.py b/plugins/CuraEngineBackend/StartSliceJob.py index 31ea999161..f3f34f4c3d 100644 --- a/plugins/CuraEngineBackend/StartSliceJob.py +++ b/plugins/CuraEngineBackend/StartSliceJob.py @@ -129,8 +129,10 @@ class StartSliceJob(Job): self.setResult(StartJobResult.MaterialIncompatible) return - for extruder_stack in ExtruderManager.getInstance().getMachineExtruders(stack.getId()): + for position, extruder_stack in stack.extruders.items(): material = extruder_stack.findContainer({"type": "material"}) + if not extruder_stack.isEnabled: + continue if material: if material.getMetaDataEntry("compatible") == False: self.setResult(StartJobResult.MaterialIncompatible) @@ -189,11 +191,18 @@ class StartSliceJob(Job): if per_object_stack: is_non_printing_mesh = any(per_object_stack.getProperty(key, "value") for key in NON_PRINTING_MESH_SETTINGS) - if node.callDecoration("getBuildPlateNumber") == self._build_plate_number: - if not getattr(node, "_outside_buildarea", False) or is_non_printing_mesh: - temp_list.append(node) - if not is_non_printing_mesh: - has_printing_mesh = True + # Find a reason not to add the node + if node.callDecoration("getBuildPlateNumber") != self._build_plate_number: + continue + if getattr(node, "_outside_buildarea", False) and not is_non_printing_mesh: + continue + node_position = node.callDecoration("getActiveExtruderPosition") + if not stack.extruders[str(node_position)].isEnabled: + continue + + temp_list.append(node) + if not is_non_printing_mesh: + has_printing_mesh = True Job.yieldThread() @@ -269,9 +278,15 @@ class StartSliceJob(Job): # \return A dictionary of replacement tokens to the values they should be # replaced with. def _buildReplacementTokens(self, stack) -> dict: + default_extruder_position = int(Application.getInstance().getMachineManager().defaultExtruderPosition) result = {} for key in stack.getAllKeys(): - result[key] = stack.getProperty(key, "value") + setting_type = stack.getProperty(key, "type") + value = stack.getProperty(key, "value") + if setting_type == "extruder" and value == -1: + # replace with the default value + value = default_extruder_position + result[key] = value Job.yieldThread() result["print_bed_temperature"] = result["material_bed_temperature"] # Renamed settings. @@ -377,11 +392,11 @@ class StartSliceJob(Job): # limit_to_extruder property. def _buildGlobalInheritsStackMessage(self, stack): for key in stack.getAllKeys(): - extruder = int(round(float(stack.getProperty(key, "limit_to_extruder")))) - if extruder >= 0: #Set to a specific extruder. + extruder_position = int(round(float(stack.getProperty(key, "limit_to_extruder")))) + if extruder_position >= 0: # Set to a specific extruder. setting_extruder = self._slice_message.addRepeatedMessage("limit_to_extruder") setting_extruder.name = key - setting_extruder.extruder = extruder + setting_extruder.extruder = extruder_position Job.yieldThread() ## Check if a node has per object settings and ensure that they are set correctly in the message diff --git a/plugins/MachineSettingsAction/MachineSettingsAction.qml b/plugins/MachineSettingsAction/MachineSettingsAction.qml index 4d00904f76..f941ef87b4 100644 --- a/plugins/MachineSettingsAction/MachineSettingsAction.qml +++ b/plugins/MachineSettingsAction/MachineSettingsAction.qml @@ -244,6 +244,7 @@ Cura.MachineAction height: childrenRect.height width: childrenRect.width text: machineExtruderCountProvider.properties.description + visible: extruderCountModel.count >= 2 Row { diff --git a/plugins/PostProcessingPlugin/PostProcessingPlugin.py b/plugins/PostProcessingPlugin/PostProcessingPlugin.py index 566b05abf3..c4b760724b 100644 --- a/plugins/PostProcessingPlugin/PostProcessingPlugin.py +++ b/plugins/PostProcessingPlugin/PostProcessingPlugin.py @@ -173,7 +173,10 @@ class PostProcessingPlugin(QObject, Extension): Logger.log("d", "Creating post processing plugin view.") ## Load all scripts in the scripts folders - for root in [PluginRegistry.getInstance().getPluginPath("PostProcessingPlugin"), Resources.getStoragePath(Resources.Preferences)]: + # The PostProcessingPlugin path is for built-in scripts. + # The Resources path is where the user should store custom scripts. + # The Preferences path is legacy, where the user may previously have stored scripts. + for root in [PluginRegistry.getInstance().getPluginPath("PostProcessingPlugin"), Resources.getStoragePath(Resources.Resources), Resources.getStoragePath(Resources.Preferences)]: path = os.path.join(root, "scripts") if not os.path.isdir(path): try: diff --git a/plugins/SolidView/SolidView.py b/plugins/SolidView/SolidView.py index 50ff2864b7..de9f922267 100644 --- a/plugins/SolidView/SolidView.py +++ b/plugins/SolidView/SolidView.py @@ -78,17 +78,13 @@ class SolidView(View): for node in DepthFirstIterator(scene.getRoot()): if not node.render(renderer): - if node.getMeshData() and node.isVisible(): + if node.getMeshData() and node.isVisible() and not node.callDecoration("getLayerData"): uniforms = {} shade_factor = 1.0 per_mesh_stack = node.callDecoration("getStack") - # Get color to render this mesh in from ExtrudersModel - extruder_index = 0 - extruder_id = node.callDecoration("getActiveExtruder") - if extruder_id: - extruder_index = max(0, self._extruders_model.find("id", extruder_id)) + extruder_index = int(node.callDecoration("getActiveExtruderPosition")) # Use the support extruder instead of the active extruder if this is a support_mesh if per_mesh_stack: diff --git a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py index 77b64ee080..70a5607071 100644 --- a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py @@ -301,8 +301,8 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): self._updatePrintJob(print_job, print_job_data) if print_job.state != "queued": # Print job should be assigned to a printer. - if print_job.state == "failed": - # Print job was failed, so don't attach it to a printer. + if print_job.state in ["failed", "finished", "aborted"]: + # Print job was already completed, so don't attach it to a printer. printer = None else: printer = self._getPrinterByKey(print_job_data["printer_uuid"]) diff --git a/plugins/VersionUpgrade/VersionUpgrade32to33/__init__.py b/plugins/VersionUpgrade/VersionUpgrade32to33/__init__.py index acccfd88dd..c411b4190e 100644 --- a/plugins/VersionUpgrade/VersionUpgrade32to33/__init__.py +++ b/plugins/VersionUpgrade/VersionUpgrade32to33/__init__.py @@ -8,10 +8,10 @@ upgrade = VersionUpgrade32to33.VersionUpgrade32to33() def getMetaData(): return { "version_upgrade": { - # From To Upgrade function + # From To Upgrade function ("definition_changes", 2000004): ("definition_changes", 3000004, upgrade.upgradeInstanceContainer), - ("quality_changes", 2000004): ("quality_changes", 3000004, upgrade.upgradeQualityChanges), - ("user", 2000004): ("user", 3000004, upgrade.upgradeInstanceContainer) + ("quality_changes", 2000004): ("quality_changes", 3000004, upgrade.upgradeQualityChanges), + ("user", 2000004): ("user", 3000004, upgrade.upgradeInstanceContainer) }, "sources": { "definition_changes": { diff --git a/plugins/XmlMaterialProfile/XmlMaterialProfile.py b/plugins/XmlMaterialProfile/XmlMaterialProfile.py index a7ba423153..bbf5dfe2ba 100644 --- a/plugins/XmlMaterialProfile/XmlMaterialProfile.py +++ b/plugins/XmlMaterialProfile/XmlMaterialProfile.py @@ -205,7 +205,7 @@ class XmlMaterialProfile(InstanceContainer): self._addSettingElement(builder, instance) machine_container_map = {} - machine_nozzle_map = {} + machine_variant_map = {} variant_manager = CuraApplication.getInstance().getVariantManager() material_manager = CuraApplication.getInstance().getMaterialManager() @@ -225,13 +225,14 @@ class XmlMaterialProfile(InstanceContainer): if definition_id not in machine_container_map: machine_container_map[definition_id] = container - if definition_id not in machine_nozzle_map: - machine_nozzle_map[definition_id] = {} + if definition_id not in machine_variant_map: + machine_variant_map[definition_id] = {} variant_name = container.getMetaDataEntry("variant_name") if variant_name: - machine_nozzle_map[definition_id][variant_name] = variant_manager.getVariantNode(definition_id, - variant_name) + variant_dict = {"variant_node": variant_manager.getVariantNode(definition_id, variant_name), + "material_container": container} + machine_variant_map[definition_id][variant_name] = variant_dict continue machine_container_map[definition_id] = container @@ -265,28 +266,60 @@ class XmlMaterialProfile(InstanceContainer): self._addSettingElement(builder, instance) # Find all hotend sub-profiles corresponding to this material and machine and add them to this profile. - for hotend_name, variant_node in machine_nozzle_map[definition_id].items(): - # The hotend identifier is not the containers name, but its "name". - builder.start("hotend", {"id": hotend_name}) + buildplate_dict = {} + for variant_name, variant_dict in machine_variant_map[definition_id].items(): + variant_type = variant_dict["variant_node"].metadata["hardware_type"] + from cura.Machines.VariantManager import VariantType + variant_type = VariantType(variant_type) + if variant_type == VariantType.NOZZLE: + # The hotend identifier is not the containers name, but its "name". + builder.start("hotend", {"id": variant_name}) - # Compatible is a special case, as it's added as a meta data entry (instead of an instance). - compatible = variant_node.metadata.get("compatible") - if compatible is not None: - builder.start("setting", {"key": "hardware compatible"}) - if compatible: - builder.data("yes") - else: - builder.data("no") - builder.end("setting") + # Compatible is a special case, as it's added as a meta data entry (instead of an instance). + material_container = variant_dict["material_container"] + compatible = container.getMetaDataEntry("compatible") + if compatible is not None: + builder.start("setting", {"key": "hardware compatible"}) + if compatible: + builder.data("yes") + else: + builder.data("no") + builder.end("setting") - for instance in variant_node.getContainer().findInstances(): - if container.getInstance(instance.definition.key) and container.getProperty(instance.definition.key, "value") == instance.value: - # If the settings match that of the machine profile, just skip since we inherit the machine profile. - continue + for instance in material_container.findInstances(): + if container.getInstance(instance.definition.key) and container.getProperty(instance.definition.key, "value") == instance.value: + # If the settings match that of the machine profile, just skip since we inherit the machine profile. + continue - self._addSettingElement(builder, instance) + self._addSettingElement(builder, instance) - builder.end("hotend") + if material_container.getMetaDataEntry("buildplate_compatible") and not buildplate_dict: + buildplate_dict["buildplate_compatible"] = material_container.getMetaDataEntry("buildplate_compatible") + buildplate_dict["buildplate_recommended"] = material_container.getMetaDataEntry("buildplate_recommended") + buildplate_dict["material_container"] = material_container + + builder.end("hotend") + + if buildplate_dict: + for variant_name in buildplate_dict["buildplate_compatible"]: + builder.start("buildplate", {"id": variant_name}) + + material_container = buildplate_dict["material_container"] + buildplate_compatible_dict = material_container.getMetaDataEntry("buildplate_compatible") + buildplate_recommended_dict = material_container.getMetaDataEntry("buildplate_recommended") + if buildplate_compatible_dict: + compatible = buildplate_compatible_dict[variant_name] + recommended = buildplate_recommended_dict[variant_name] + + builder.start("setting", {"key": "hardware compatible"}) + builder.data("yes" if compatible else "no") + builder.end("setting") + + builder.start("setting", {"key": "hardware recommended"}) + builder.data("yes" if recommended else "no") + builder.end("setting") + + builder.end("buildplate") builder.end("machine") @@ -617,7 +650,8 @@ class XmlMaterialProfile(InstanceContainer): from cura.Machines.VariantManager import VariantType variant_manager = CuraApplication.getInstance().getVariantManager() - variant_node = variant_manager.getVariantNode(machine_id, buildplate_id) + variant_node = variant_manager.getVariantNode(machine_id, buildplate_id, + variant_type = VariantType.BUILD_PLATE) if not variant_node: continue @@ -841,6 +875,8 @@ class XmlMaterialProfile(InstanceContainer): continue settings = buildplate.iterfind("./um:setting", cls.__namespaces) + buildplate_compatibility = True + buildplate_recommended = True for entry in settings: key = entry.get("key") if key == "hardware compatible": @@ -848,8 +884,8 @@ class XmlMaterialProfile(InstanceContainer): elif key == "hardware recommended": buildplate_recommended = cls._parseCompatibleValue(entry.text) - buildplate_map["buildplate_compatible"][buildplate_id] = buildplate_map["buildplate_compatible"] - buildplate_map["buildplate_recommended"][buildplate_id] = buildplate_map["buildplate_recommended"] + buildplate_map["buildplate_compatible"][buildplate_id] = buildplate_compatibility + buildplate_map["buildplate_recommended"][buildplate_id] = buildplate_recommended for hotend in machine.iterfind("./um:hotend", cls.__namespaces): hotend_name = hotend.get("id") diff --git a/resources/definitions/fdmprinter.def.json b/resources/definitions/fdmprinter.def.json index 12ba59b8a1..6d4688bb52 100644 --- a/resources/definitions/fdmprinter.def.json +++ b/resources/definitions/fdmprinter.def.json @@ -211,6 +211,18 @@ "settable_per_extruder": false, "settable_per_meshgroup": false }, + "extruders_enabled_count": + { + "label": "Number of Extruders that are enabled", + "description": "Number of extruder trains that are enabled; automatically set in software", + "default_value": "machine_extruder_count", + "minimum_value": "1", + "maximum_value": "16", + "type": "int", + "settable_per_mesh": false, + "settable_per_extruder": false, + "settable_per_meshgroup": false + }, "machine_nozzle_tip_outer_diameter": { "label": "Outer nozzle diameter", @@ -887,7 +899,7 @@ "settable_per_extruder": false, "settable_per_meshgroup": true, "settable_globally": true, - "enabled": "machine_extruder_count > 1", + "enabled": "extruders_enabled_count > 1", "children": { "wall_0_extruder_nr": { @@ -900,7 +912,7 @@ "settable_per_extruder": false, "settable_per_meshgroup": true, "settable_globally": true, - "enabled": "machine_extruder_count > 1" + "enabled": "extruders_enabled_count > 1" }, "wall_x_extruder_nr": { @@ -913,7 +925,7 @@ "settable_per_extruder": false, "settable_per_meshgroup": true, "settable_globally": true, - "enabled": "machine_extruder_count > 1" + "enabled": "extruders_enabled_count > 1" } } }, @@ -970,7 +982,7 @@ "settable_per_extruder": false, "settable_per_meshgroup": true, "settable_globally": true, - "enabled": "machine_extruder_count > 1 and max(extruderValues('roofing_layer_count')) > 0 and max(extruderValues('top_layers')) > 0" + "enabled": "extruders_enabled_count > 1 and max(extruderValues('roofing_layer_count')) > 0 and max(extruderValues('top_layers')) > 0" }, "roofing_layer_count": { @@ -995,7 +1007,7 @@ "settable_per_extruder": false, "settable_per_meshgroup": true, "settable_globally": true, - "enabled": "machine_extruder_count > 1" + "enabled": "extruders_enabled_count > 1" }, "top_bottom_thickness": { @@ -1465,7 +1477,7 @@ "settable_per_extruder": false, "settable_per_meshgroup": true, "settable_globally": true, - "enabled": "machine_extruder_count > 1" + "enabled": "extruders_enabled_count > 1" }, "infill_sparse_density": { @@ -1916,7 +1928,7 @@ "minimum_value": "0", "maximum_value_warning": "10.0", "maximum_value": "machine_nozzle_heat_up_speed", - "enabled": "material_flow_dependent_temperature or (machine_extruder_count > 1 and material_final_print_temperature != material_print_temperature)", + "enabled": "material_flow_dependent_temperature or (extruders_enabled_count > 1 and material_final_print_temperature != material_print_temperature)", "settable_per_mesh": false, "settable_per_extruder": true }, @@ -2179,7 +2191,7 @@ "minimum_value": "-273.15", "minimum_value_warning": "0", "maximum_value_warning": "260", - "enabled": "machine_extruder_count > 1 and machine_nozzle_temp_enabled", + "enabled": "extruders_enabled_count > 1 and machine_nozzle_temp_enabled", "settable_per_mesh": false, "settable_per_extruder": true }, @@ -3281,7 +3293,7 @@ "description": "After the machine switched from one extruder to the other, the build plate is lowered to create clearance between the nozzle and the print. This prevents the nozzle from leaving oozed material on the outside of a print.", "type": "bool", "default_value": true, - "enabled": "retraction_hop_enabled and machine_extruder_count > 1", + "enabled": "retraction_hop_enabled and extruders_enabled_count > 1", "settable_per_mesh": false, "settable_per_extruder": true } @@ -3459,7 +3471,8 @@ "description": "The extruder train to use for printing the support. This is used in multi-extrusion.", "type": "extruder", "default_value": "0", - "enabled": "(support_enable or support_tree_enable) and machine_extruder_count > 1", + "value": "-1", + "enabled": "(support_enable or support_tree_enable) and extruders_enabled_count > 1", "settable_per_mesh": false, "settable_per_extruder": false, "children": { @@ -3470,7 +3483,7 @@ "type": "extruder", "default_value": "0", "value": "support_extruder_nr", - "enabled": "(support_enable or support_tree_enable) and machine_extruder_count > 1", + "enabled": "(support_enable or support_tree_enable) and extruders_enabled_count > 1", "settable_per_mesh": false, "settable_per_extruder": false }, @@ -3481,7 +3494,7 @@ "type": "extruder", "default_value": "0", "value": "support_extruder_nr", - "enabled": "(support_enable or support_tree_enable) and machine_extruder_count > 1", + "enabled": "(support_enable or support_tree_enable) and extruders_enabled_count > 1", "settable_per_mesh": false, "settable_per_extruder": false }, @@ -3492,7 +3505,7 @@ "type": "extruder", "default_value": "0", "value": "support_extruder_nr", - "enabled": "(support_enable or support_tree_enable) and machine_extruder_count > 1", + "enabled": "(support_enable or support_tree_enable) and extruders_enabled_count > 1", "settable_per_mesh": false, "settable_per_extruder": false, "children": @@ -3504,7 +3517,7 @@ "type": "extruder", "default_value": "0", "value": "support_interface_extruder_nr", - "enabled": "(support_enable or support_tree_enable) and machine_extruder_count > 1", + "enabled": "(support_enable or support_tree_enable) and extruders_enabled_count > 1", "settable_per_mesh": false, "settable_per_extruder": false }, @@ -3515,7 +3528,7 @@ "type": "extruder", "default_value": "0", "value": "support_interface_extruder_nr", - "enabled": "(support_enable or support_tree_enable) and machine_extruder_count > 1", + "enabled": "(support_enable or support_tree_enable) and extruders_enabled_count > 1", "settable_per_mesh": false, "settable_per_extruder": false } @@ -4185,7 +4198,8 @@ "description": "The extruder train to use for printing the skirt/brim/raft. This is used in multi-extrusion.", "type": "extruder", "default_value": "0", - "enabled": "machine_extruder_count > 1 and resolveOrValue('adhesion_type') != 'none'", + "value": "-1", + "enabled": "extruders_enabled_count > 1 and resolveOrValue('adhesion_type') != 'none'", "settable_per_mesh": false, "settable_per_extruder": false }, @@ -4756,7 +4770,7 @@ "label": "Enable Prime Tower", "description": "Print a tower next to the print which serves to prime the material after each nozzle switch.", "type": "bool", - "enabled": "machine_extruder_count > 1", + "enabled": "extruders_enabled_count > 1", "default_value": false, "resolve": "any(extruderValues('prime_tower_enable'))", "settable_per_mesh": false, @@ -4904,7 +4918,7 @@ "description": "Enable exterior ooze shield. This will create a shell around the model which is likely to wipe a second nozzle if it's at the same height as the first nozzle.", "type": "bool", "resolve": "any(extruderValues('ooze_shield_enabled'))", - "enabled": "machine_extruder_count > 1", + "enabled": "extruders_enabled_count > 1", "default_value": false, "settable_per_mesh": false, "settable_per_extruder": false @@ -4997,7 +5011,7 @@ "description": "Remove areas where multiple meshes are overlapping with each other. This may be used if merged dual material objects overlap with each other.", "type": "bool", "default_value": true, - "value": "machine_extruder_count > 1", + "value": "extruders_enabled_count > 1", "settable_per_mesh": false, "settable_per_extruder": false, "settable_per_meshgroup": true @@ -5044,7 +5058,7 @@ "one_at_a_time": "One at a Time" }, "default_value": "all_at_once", - "enabled": "machine_extruder_count == 1", + "enabled": "extruders_enabled_count == 1", "settable_per_mesh": false, "settable_per_extruder": false, "settable_per_meshgroup": false diff --git a/resources/definitions/ubuild-3d_mr_bot_280.def.json b/resources/definitions/ubuild-3d_mr_bot_280.def.json index 7f9069370c..4febdcd350 100644 --- a/resources/definitions/ubuild-3d_mr_bot_280.def.json +++ b/resources/definitions/ubuild-3d_mr_bot_280.def.json @@ -11,7 +11,8 @@ "file_formats": "text/x-gcode", "icon": "icon_uBuild-3D", "platform": "mr_bot_280_platform.stl", - "has_materials": true + "has_materials": true, + "preferred_quality_type": "draft" }, "overrides": { @@ -24,7 +25,6 @@ "material_diameter": { "default_value": 1.75 }, "material_bed_temperature": { "default_value": 70 }, "machine_nozzle_size": { "default_value": 0.4 }, - "layer_height": { "default_value": 0.2 }, "layer_height_0": { "default_value": 0.1 }, "retraction_amount": { "default_value": 2 }, "retraction_speed": { "default_value": 50 }, diff --git a/resources/definitions/ultimaker3.def.json b/resources/definitions/ultimaker3.def.json index dcf6b167c0..4e2b5ad4c2 100644 --- a/resources/definitions/ultimaker3.def.json +++ b/resources/definitions/ultimaker3.def.json @@ -43,10 +43,10 @@ { "default_value": [ - [ -29, 6.1 ], - [ -29, -33.9 ], - [ 71, 6.1 ], - [ 71, -33.9 ] + [ -41.9, -45.8 ], + [ -41.9, 33.9 ], + [ 59.9, 33.9 ], + [ 59.9, -45.8 ] ] }, "machine_gcode_flavor": { "default_value": "Griffin" }, diff --git a/resources/qml/Cura.qml b/resources/qml/Cura.qml index a8066e01e4..c81281f0d7 100644 --- a/resources/qml/Cura.qml +++ b/resources/qml/Cura.qml @@ -194,14 +194,31 @@ UM.MainWindow NozzleMenu { title: Cura.MachineManager.activeDefinitionVariantsName; visible: Cura.MachineManager.hasVariants; extruderIndex: index } MaterialMenu { title: catalog.i18nc("@title:menu", "&Material"); visible: Cura.MachineManager.hasMaterials; extruderIndex: index } - MenuSeparator { + MenuSeparator + { visible: Cura.MachineManager.hasVariants || Cura.MachineManager.hasMaterials } - MenuItem { + MenuItem + { text: catalog.i18nc("@action:inmenu", "Set as Active Extruder") - onTriggered: Cura.ExtruderManager.setActiveExtruderIndex(model.index) + onTriggered: Cura.MachineManager.setExtruderIndex(model.index) } + + MenuItem + { + text: catalog.i18nc("@action:inmenu", "Enable Extruder") + onTriggered: Cura.MachineManager.setExtruderEnabled(model.index, true) + visible: !Cura.MachineManager.getExtruder(model.index).isEnabled + } + + MenuItem + { + text: catalog.i18nc("@action:inmenu", "Disable Extruder") + onTriggered: Cura.MachineManager.setExtruderEnabled(model.index, false) + visible: Cura.MachineManager.getExtruder(model.index).isEnabled + } + } onObjectAdded: settingsMenu.insertItem(index, object) onObjectRemoved: settingsMenu.removeItem(object) diff --git a/resources/qml/ExtruderButton.qml b/resources/qml/ExtruderButton.qml index e4350e0a2c..2c1b80047e 100644 --- a/resources/qml/ExtruderButton.qml +++ b/resources/qml/ExtruderButton.qml @@ -19,7 +19,7 @@ Button iconSource: UM.Theme.getIcon("extruder_button") checked: Cura.ExtruderManager.selectedObjectExtruders.indexOf(extruder.id) != -1 - enabled: UM.Selection.hasSelection + enabled: UM.Selection.hasSelection && extruder.stack.isEnabled property color customColor: base.hovered ? UM.Theme.getColor("button_hover") : UM.Theme.getColor("button"); diff --git a/resources/qml/Preferences/MaterialsPage.qml b/resources/qml/Preferences/MaterialsPage.qml index 553cfe0423..7f06ffecde 100644 --- a/resources/qml/Preferences/MaterialsPage.qml +++ b/resources/qml/Preferences/MaterialsPage.qml @@ -19,14 +19,17 @@ Item UM.I18nCatalog { id: catalog; name: "cura"; } - Cura.MaterialManagementModel { + Cura.MaterialManagementModel + { id: materialsModel } - Label { + Label + { id: titleLabel - anchors { + anchors + { top: parent.top left: parent.left right: parent.right @@ -170,22 +173,27 @@ Item Connections { target: materialsModel - onItemsChanged: { + onItemsChanged: + { var currentItemId = base.currentItem == null ? "" : base.currentItem.root_material_id; var position = Cura.ExtruderManager.activeExtruderIndex; // try to pick the currently selected item; it may have been moved - if (base.newRootMaterialIdToSwitchTo == "") { + if (base.newRootMaterialIdToSwitchTo == "") + { base.newRootMaterialIdToSwitchTo = currentItemId; } - for (var idx = 0; idx < materialsModel.rowCount(); ++idx) { + for (var idx = 0; idx < materialsModel.rowCount(); ++idx) + { var item = materialsModel.getItem(idx); - if (item.root_material_id == base.newRootMaterialIdToSwitchTo) { + if (item.root_material_id == base.newRootMaterialIdToSwitchTo) + { // Switch to the newly created profile if needed materialListView.currentIndex = idx; materialListView.activateDetailsWithIndex(materialListView.currentIndex); - if (base.toActivateNewMaterial) { + if (base.toActivateNewMaterial) + { Cura.MachineManager.setMaterial(position, item.container_node); } base.newRootMaterialIdToSwitchTo = ""; @@ -196,7 +204,8 @@ Item materialListView.currentIndex = 0; materialListView.activateDetailsWithIndex(materialListView.currentIndex); - if (base.toActivateNewMaterial) { + if (base.toActivateNewMaterial) + { Cura.MachineManager.setMaterial(position, materialsModel.getItem(0).container_node); } base.newRootMaterialIdToSwitchTo = ""; @@ -233,14 +242,17 @@ Item messageDialog.title = catalog.i18nc("@title:window", "Import Material"); messageDialog.text = catalog.i18nc("@info:status Don't translate the XML tags or !", "Could not import material %1: %2").arg(fileUrl).arg(result.message); - if (result.status == "success") { + if (result.status == "success") + { messageDialog.icon = StandardIcon.Information; messageDialog.text = catalog.i18nc("@info:status Don't translate the XML tag !", "Successfully imported material %1").arg(fileUrl); } - else if (result.status == "duplicate") { + else if (result.status == "duplicate") + { messageDialog.icon = StandardIcon.Warning; } - else { + else + { messageDialog.icon = StandardIcon.Critical; } messageDialog.open(); @@ -260,12 +272,14 @@ Item var result = Cura.ContainerManager.exportContainer(base.currentItem.root_material_id, selectedNameFilter, fileUrl); messageDialog.title = catalog.i18nc("@title:window", "Export Material"); - if (result.status == "error") { + if (result.status == "error") + { messageDialog.icon = StandardIcon.Critical; messageDialog.text = catalog.i18nc("@info:status Don't translate the XML tags and !", "Failed to export material to %1: %2").arg(fileUrl).arg(result.message); messageDialog.open(); } - else if (result.status == "success") { + else if (result.status == "success") + { messageDialog.icon = StandardIcon.Information; messageDialog.text = catalog.i18nc("@info:status Don't translate the XML tag !", "Successfully exported material to %1").arg(result.path); messageDialog.open(); @@ -283,7 +297,8 @@ Item Item { id: contentsItem - anchors { + anchors + { top: titleLabel.bottom left: parent.left right: parent.right @@ -297,7 +312,8 @@ Item Item { - anchors { + anchors + { top: buttonRow.bottom topMargin: UM.Theme.getSize("default_margin").height left: parent.left @@ -310,12 +326,14 @@ Item Label { id: captionLabel - anchors { + anchors + { top: parent.top left: parent.left } visible: text != "" - text: { + text: + { var caption = catalog.i18nc("@action:label", "Printer") + ": " + Cura.MachineManager.activeMachineName; if (Cura.MachineManager.hasVariants) { @@ -330,14 +348,16 @@ Item ScrollView { id: materialScrollView - anchors { + anchors + { top: captionLabel.visible ? captionLabel.bottom : parent.top topMargin: captionLabel.visible ? UM.Theme.getSize("default_margin").height : 0 bottom: parent.bottom left: parent.left } - Rectangle { + Rectangle + { parent: viewport anchors.fill: parent color: palette.light @@ -418,13 +438,15 @@ Item MouseArea { anchors.fill: parent - onClicked: { + onClicked: + { parent.ListView.view.currentIndex = model.index; } } } - function activateDetailsWithIndex(index) { + function activateDetailsWithIndex(index) + { var model = materialsModel.getItem(index); base.currentItem = model; materialDetailsView.containerId = model.container_id; @@ -446,7 +468,8 @@ Item { id: detailsPanel - anchors { + anchors + { left: materialScrollView.right leftMargin: UM.Theme.getSize("default_margin").width top: parent.top diff --git a/resources/qml/Preferences/ProfileTab.qml b/resources/qml/Preferences/ProfileTab.qml index e202e933f3..0ae0899051 100644 --- a/resources/qml/Preferences/ProfileTab.qml +++ b/resources/qml/Preferences/ProfileTab.qml @@ -11,7 +11,7 @@ Tab { id: base - property string extruderPosition: "" + property int extruderPosition: -1 //Denotes the global stack. property var qualityItem: null property bool isQualityItemCurrentlyActivated: diff --git a/resources/qml/Settings/SettingExtruder.qml b/resources/qml/Settings/SettingExtruder.qml index 17a0dd5eee..2ddbb135c7 100644 --- a/resources/qml/Settings/SettingExtruder.qml +++ b/resources/qml/Settings/SettingExtruder.qml @@ -17,14 +17,39 @@ SettingItem id: control anchors.fill: parent - model: Cura.ExtrudersModel { onModelChanged: control.color = getItem(control.currentIndex).color } + model: Cura.ExtrudersModel + { + onModelChanged: { + control.color = getItem(control.currentIndex).color; + } + } textRole: "name" + // knowing the extruder position, try to find the item index in the model + function getIndexByPosition(position) + { + for (var item_index in model.items) + { + var item = model.getItem(item_index) + if (item.index == position) + { + return item_index + } + } + return -1 + } + onActivated: { - forceActiveFocus(); - propertyProvider.setPropertyValue("value", model.getItem(index).index); + if (model.getItem(index).enabled) + { + forceActiveFocus(); + propertyProvider.setPropertyValue("value", model.getItem(index).index); + } else + { + currentIndex = propertyProvider.properties.value; // keep the old value + } } onActiveFocusChanged: @@ -64,6 +89,23 @@ SettingItem value: control.currentText != "" ? control.model.getItem(control.currentIndex).color : "" } + Binding + { + target: control + property: "currentIndex" + value: + { + if(propertyProvider.properties.value == -1) + { + return control.getIndexByPosition(Cura.MachineManager.defaultExtruderPosition); + } + return propertyProvider.properties.value + } + // Sometimes when the value is already changed, the model is still being built. + // The when clause ensures that the current index is not updated when this happens. + when: control.model.items.length > 0 + } + indicator: UM.RecolorImage { id: downArrow @@ -173,7 +215,13 @@ SettingItem { text: model.name renderType: Text.NativeRendering - color: UM.Theme.getColor("setting_control_text") + color: { + if (model.enabled) { + UM.Theme.getColor("setting_control_text") + } else { + UM.Theme.getColor("action_button_disabled_text"); + } + } font: UM.Theme.getFont("default") elide: Text.ElideRight verticalAlignment: Text.AlignVCenter diff --git a/resources/qml/SidebarHeader.qml b/resources/qml/SidebarHeader.qml index c93dd3a89b..baceb5f683 100644 --- a/resources/qml/SidebarHeader.qml +++ b/resources/qml/SidebarHeader.qml @@ -143,10 +143,42 @@ Column exclusiveGroup: extruderMenuGroup checked: base.currentExtruderIndex == index - onClicked: + property bool extruder_enabled: true + + MouseArea { - forceActiveFocus() // Changing focus applies the currently-being-typed values so it can change the displayed setting values. - Cura.ExtruderManager.setActiveExtruderIndex(index); + anchors.fill: parent + acceptedButtons: Qt.LeftButton | Qt.RightButton + onClicked: { + switch (mouse.button) { + case Qt.LeftButton: + forceActiveFocus(); // Changing focus applies the currently-being-typed values so it can change the displayed setting values. + Cura.ExtruderManager.setActiveExtruderIndex(index); + break; + case Qt.RightButton: + extruder_enabled = Cura.MachineManager.getExtruder(model.index).isEnabled + extruderMenu.popup(); + break; + } + + } + } + + Menu + { + id: extruderMenu + + MenuItem { + text: catalog.i18nc("@action:inmenu", "Enable Extruder") + onTriggered: Cura.MachineManager.setExtruderEnabled(model.index, true) + visible: !extruder_enabled // using an intermediate variable prevents an empty popup that occured now and then + } + + MenuItem { + text: catalog.i18nc("@action:inmenu", "Disable Extruder") + onTriggered: Cura.MachineManager.setExtruderEnabled(model.index, false) + visible: extruder_enabled + } } style: ButtonStyle @@ -166,6 +198,18 @@ Column Behavior on color { ColorAnimation { duration: 50; } } } + function buttonColor(index) { + var extruder = Cura.MachineManager.getExtruder(index); + if (extruder.isEnabled) { + return ( + control.checked || control.pressed) ? UM.Theme.getColor("action_button_active_text") : + control.hovered ? UM.Theme.getColor("action_button_hovered_text") : + UM.Theme.getColor("action_button_text"); + } else { + return UM.Theme.getColor("action_button_disabled_text"); + } + } + Item { id: extruderButtonFace @@ -183,9 +227,7 @@ Column anchors.verticalCenter: parent.verticalCenter anchors.left: parent.left - color: (control.checked || control.pressed) ? UM.Theme.getColor("action_button_active_text") : - control.hovered ? UM.Theme.getColor("action_button_hovered_text") : - UM.Theme.getColor("action_button_text") + color: buttonColor(index) font: UM.Theme.getFont("large_nonbold") text: catalog.i18nc("@label", "Extruder") @@ -228,9 +270,7 @@ Column id: extruderNumberText anchors.centerIn: parent text: index + 1; - color: (control.checked || control.pressed) ? UM.Theme.getColor("action_button_active_text") : - control.hovered ? UM.Theme.getColor("action_button_hovered_text") : - UM.Theme.getColor("action_button_text") + color: buttonColor(index) font: UM.Theme.getFont("default_bold") } diff --git a/resources/qml/SidebarSimple.qml b/resources/qml/SidebarSimple.qml index cef6f38b4b..a7c8a1b8c5 100644 --- a/resources/qml/SidebarSimple.qml +++ b/resources/qml/SidebarSimple.qml @@ -19,7 +19,7 @@ Item property Action configureSettings; property variant minimumPrintTime: PrintInformation.minimumPrintTime; property variant maximumPrintTime: PrintInformation.maximumPrintTime; - property bool settingsEnabled: Cura.ExtruderManager.activeExtruderStackId || machineExtruderCount.properties.value == 1 + property bool settingsEnabled: Cura.ExtruderManager.activeExtruderStackId || extrudersEnabledCount.properties.value == 1 Component.onCompleted: PrintInformation.enabled = true Component.onDestruction: PrintInformation.enabled = false @@ -67,10 +67,8 @@ Item Connections { - target: Cura.MachineManager - onActiveQualityChanged: qualityModel.update() - onActiveMaterialChanged: qualityModel.update() - onActiveVariantChanged: qualityModel.update() + target: Cura.QualityProfilesDropDownMenuModel + onItemsChanged: qualityModel.update() } Connections { @@ -518,7 +516,12 @@ Item // Update the slider value to represent the rounded value infillSlider.value = roundedSliderValue - Cura.MachineManager.setSettingForAllExtruders("infill_sparse_density", "value", roundedSliderValue) + // Update value only if the Recomended mode is Active, + // Otherwise if I change the value in the Custom mode the Recomended view will try to repeat + // same operation + if (UM.Preferences.getValue("cura/active_mode") == 0) { + Cura.MachineManager.setSettingForAllExtruders("infill_sparse_density", "value", roundedSliderValue) + } } style: SliderStyle @@ -788,25 +791,10 @@ Item } } - Label - { - id: supportExtruderLabel - visible: supportExtruderCombobox.visible - anchors.left: parent.left - anchors.leftMargin: UM.Theme.getSize("sidebar_margin").width - anchors.right: infillCellLeft.right - anchors.rightMargin: UM.Theme.getSize("sidebar_margin").width - anchors.verticalCenter: supportExtruderCombobox.verticalCenter - text: catalog.i18nc("@label", "Support Extruder"); - font: UM.Theme.getFont("default"); - color: UM.Theme.getColor("text"); - elide: Text.ElideRight - } - ComboBox { id: supportExtruderCombobox - visible: enableSupportCheckBox.visible && (supportEnabled.properties.value == "True") && (machineExtruderCount.properties.value > 1) + visible: enableSupportCheckBox.visible && (supportEnabled.properties.value == "True") && (extrudersEnabledCount.properties.value > 1) model: extruderModel property string color_override: "" // for manually setting values @@ -820,11 +808,12 @@ Item textRole: "text" // this solves that the combobox isn't populated in the first time Cura is started - anchors.top: enableSupportCheckBox.bottom - anchors.topMargin: ((supportEnabled.properties.value === "True") && (machineExtruderCount.properties.value > 1)) ? UM.Theme.getSize("sidebar_margin").height : 0 - anchors.left: infillCellRight.left + anchors.top: enableSupportCheckBox.top + //anchors.topMargin: ((supportEnabled.properties.value === "True") && (machineExtruderCount.properties.value > 1)) ? UM.Theme.getSize("sidebar_margin").height : 0 + anchors.left: enableSupportCheckBox.right + anchors.leftMargin: Math.round(UM.Theme.getSize("sidebar_margin").width / 2) - width: Math.round(UM.Theme.getSize("sidebar").width * .55) + width: Math.round(UM.Theme.getSize("sidebar").width * .55) - Math.round(UM.Theme.getSize("sidebar_margin").width / 2) - enableSupportCheckBox.width height: ((supportEnabled.properties.value == "True") && (machineExtruderCount.properties.value > 1)) ? UM.Theme.getSize("setting_control").height : 0 Behavior on height { NumberAnimation { duration: 100 } } @@ -891,7 +880,7 @@ Item id: adhesionCheckBox property alias _hovered: adhesionMouseArea.containsMouse - anchors.top: enableSupportCheckBox.visible ? supportExtruderCombobox.bottom : infillCellRight.bottom + anchors.top: enableSupportCheckBox.bottom anchors.topMargin: UM.Theme.getSize("sidebar_margin").height anchors.left: infillCellRight.left @@ -1022,9 +1011,9 @@ Item UM.SettingPropertyProvider { - id: machineExtruderCount + id: extrudersEnabledCount containerStackId: Cura.MachineManager.activeMachineId - key: "machine_extruder_count" + key: "extruders_enabled_count" watchedProperties: [ "value" ] storeIndex: 0 }