1# Copyright (c) 2020 Ultimaker B.V. 2# Cura is released under the terms of the LGPLv3 or higher. 3 4from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, QVariant # For communicating data and events to Qt. 5from UM.FlameProfiler import pyqtSlot 6 7import cura.CuraApplication # To get the global container stack to find the current machine. 8from cura.Settings.GlobalStack import GlobalStack 9from UM.Logger import Logger 10from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator 11from UM.Scene.SceneNode import SceneNode 12from UM.Scene.Selection import Selection 13from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator 14from UM.Settings.ContainerRegistry import ContainerRegistry # Finding containers by ID. 15 16from typing import Any, cast, Dict, List, Optional, TYPE_CHECKING, Union 17 18if TYPE_CHECKING: 19 from cura.Settings.ExtruderStack import ExtruderStack 20 21 22class ExtruderManager(QObject): 23 """Manages all existing extruder stacks. 24 25 This keeps a list of extruder stacks for each machine. 26 """ 27 28 def __init__(self, parent = None): 29 """Registers listeners and such to listen to changes to the extruders.""" 30 31 if ExtruderManager.__instance is not None: 32 raise RuntimeError("Try to create singleton '%s' more than once" % self.__class__.__name__) 33 ExtruderManager.__instance = self 34 35 super().__init__(parent) 36 37 self._application = cura.CuraApplication.CuraApplication.getInstance() 38 39 # Per machine, a dictionary of extruder container stack IDs. Only for separately defined extruders. 40 self._extruder_trains = {} # type: Dict[str, Dict[str, "ExtruderStack"]] 41 self._active_extruder_index = -1 # Indicates the index of the active extruder stack. -1 means no active extruder stack 42 43 # TODO; I have no idea why this is a union of ID's and extruder stacks. This needs to be fixed at some point. 44 self._selected_object_extruders = [] # type: List[Union[str, "ExtruderStack"]] 45 46 Selection.selectionChanged.connect(self.resetSelectedObjectExtruders) 47 48 extrudersChanged = pyqtSignal(QVariant) 49 """Signal to notify other components when the list of extruders for a machine definition changes.""" 50 51 activeExtruderChanged = pyqtSignal() 52 """Notify when the user switches the currently active extruder.""" 53 54 @pyqtProperty(str, notify = activeExtruderChanged) 55 def activeExtruderStackId(self) -> Optional[str]: 56 """Gets the unique identifier of the currently active extruder stack. 57 58 The currently active extruder stack is the stack that is currently being 59 edited. 60 61 :return: The unique ID of the currently active extruder stack. 62 """ 63 64 if not self._application.getGlobalContainerStack(): 65 return None # No active machine, so no active extruder. 66 try: 67 return self._extruder_trains[self._application.getGlobalContainerStack().getId()][str(self.activeExtruderIndex)].getId() 68 except KeyError: # Extruder index could be -1 if the global tab is selected, or the entry doesn't exist if the machine definition is wrong. 69 return None 70 71 @pyqtProperty("QVariantMap", notify = extrudersChanged) 72 def extruderIds(self) -> Dict[str, str]: 73 """Gets a dict with the extruder stack ids with the extruder number as the key.""" 74 75 extruder_stack_ids = {} # type: Dict[str, str] 76 77 global_container_stack = self._application.getGlobalContainerStack() 78 if global_container_stack: 79 extruder_stack_ids = {extruder.getMetaDataEntry("position", ""): extruder.id for extruder in global_container_stack.extruderList} 80 81 return extruder_stack_ids 82 83 @pyqtSlot(int) 84 def setActiveExtruderIndex(self, index: int) -> None: 85 """Changes the active extruder by index. 86 87 :param index: The index of the new active extruder. 88 """ 89 90 if self._active_extruder_index != index: 91 self._active_extruder_index = index 92 self.activeExtruderChanged.emit() 93 94 @pyqtProperty(int, notify = activeExtruderChanged) 95 def activeExtruderIndex(self) -> int: 96 return self._active_extruder_index 97 98 selectedObjectExtrudersChanged = pyqtSignal() 99 """Emitted whenever the selectedObjectExtruders property changes.""" 100 101 @pyqtProperty("QVariantList", notify = selectedObjectExtrudersChanged) 102 def selectedObjectExtruders(self) -> List[Union[str, "ExtruderStack"]]: 103 """Provides a list of extruder IDs used by the current selected objects.""" 104 105 if not self._selected_object_extruders: 106 object_extruders = set() 107 108 # First, build a list of the actual selected objects (including children of groups, excluding group nodes) 109 selected_nodes = [] # type: List["SceneNode"] 110 for node in Selection.getAllSelectedObjects(): 111 if node.callDecoration("isGroup"): 112 for grouped_node in BreadthFirstIterator(node): 113 if grouped_node.callDecoration("isGroup"): 114 continue 115 116 selected_nodes.append(grouped_node) 117 else: 118 selected_nodes.append(node) 119 120 # Then, figure out which nodes are used by those selected nodes. 121 current_extruder_trains = self.getActiveExtruderStacks() 122 for node in selected_nodes: 123 extruder = node.callDecoration("getActiveExtruder") 124 if extruder: 125 object_extruders.add(extruder) 126 elif current_extruder_trains: 127 object_extruders.add(current_extruder_trains[0].getId()) 128 129 self._selected_object_extruders = list(object_extruders) 130 131 return self._selected_object_extruders 132 133 def resetSelectedObjectExtruders(self) -> None: 134 """Reset the internal list used for the selectedObjectExtruders property 135 136 This will trigger a recalculation of the extruders used for the 137 selection. 138 """ 139 140 self._selected_object_extruders = [] 141 self.selectedObjectExtrudersChanged.emit() 142 143 @pyqtSlot(result = QObject) 144 def getActiveExtruderStack(self) -> Optional["ExtruderStack"]: 145 return self.getExtruderStack(self.activeExtruderIndex) 146 147 def getExtruderStack(self, index) -> Optional["ExtruderStack"]: 148 """Get an extruder stack by index""" 149 150 global_container_stack = self._application.getGlobalContainerStack() 151 if global_container_stack: 152 if global_container_stack.getId() in self._extruder_trains: 153 if str(index) in self._extruder_trains[global_container_stack.getId()]: 154 return self._extruder_trains[global_container_stack.getId()][str(index)] 155 return None 156 157 def getAllExtruderSettings(self, setting_key: str, prop: str) -> List[Any]: 158 """Gets a property of a setting for all extruders. 159 160 :param setting_key: :type{str} The setting to get the property of. 161 :param prop: :type{str} The property to get. 162 :return: :type{List} the list of results 163 """ 164 165 result = [] 166 167 for extruder_stack in self.getActiveExtruderStacks(): 168 result.append(extruder_stack.getProperty(setting_key, prop)) 169 170 return result 171 172 def extruderValueWithDefault(self, value: str) -> str: 173 machine_manager = self._application.getMachineManager() 174 if value == "-1": 175 return machine_manager.defaultExtruderPosition 176 else: 177 return value 178 179 def getUsedExtruderStacks(self) -> List["ExtruderStack"]: 180 """Gets the extruder stacks that are actually being used at the moment. 181 182 An extruder stack is being used if it is the extruder to print any mesh 183 with, or if it is the support infill extruder, the support interface 184 extruder, or the bed adhesion extruder. 185 186 If there are no extruders, this returns the global stack as a singleton 187 list. 188 189 :return: A list of extruder stacks. 190 """ 191 192 global_stack = self._application.getGlobalContainerStack() 193 container_registry = ContainerRegistry.getInstance() 194 195 used_extruder_stack_ids = set() 196 197 # Get the extruders of all meshes in the scene 198 support_enabled = False 199 support_bottom_enabled = False 200 support_roof_enabled = False 201 202 scene_root = self._application.getController().getScene().getRoot() 203 204 # If no extruders are registered in the extruder manager yet, return an empty array 205 if len(self.extruderIds) == 0: 206 return [] 207 number_active_extruders = len([extruder for extruder in self.getActiveExtruderStacks() if extruder.isEnabled]) 208 209 # Get the extruders of all printable meshes in the scene 210 nodes = [node for node in DepthFirstIterator(scene_root) if node.isSelectable() and not node.callDecoration("isAntiOverhangMesh") and not node.callDecoration("isSupportMesh")] #type: ignore #Ignore type error because iter() should get called automatically by Python syntax. 211 212 for node in nodes: 213 extruder_stack_id = node.callDecoration("getActiveExtruder") 214 if not extruder_stack_id: 215 # No per-object settings for this node 216 extruder_stack_id = self.extruderIds["0"] 217 used_extruder_stack_ids.add(extruder_stack_id) 218 219 if len(used_extruder_stack_ids) == number_active_extruders: 220 # We're already done. Stop looking. 221 # Especially with a lot of models on the buildplate, this will speed up things rather dramatically. 222 break 223 224 # Get whether any of them use support. 225 stack_to_use = node.callDecoration("getStack") # if there is a per-mesh stack, we use it 226 if not stack_to_use: 227 # if there is no per-mesh stack, we use the build extruder for this mesh 228 stack_to_use = container_registry.findContainerStacks(id = extruder_stack_id)[0] 229 230 if not support_enabled: 231 support_enabled |= stack_to_use.getProperty("support_enable", "value") 232 if not support_bottom_enabled: 233 support_bottom_enabled |= stack_to_use.getProperty("support_bottom_enable", "value") 234 if not support_roof_enabled: 235 support_roof_enabled |= stack_to_use.getProperty("support_roof_enable", "value") 236 237 # Check limit to extruders 238 limit_to_extruder_feature_list = ["wall_0_extruder_nr", 239 "wall_x_extruder_nr", 240 "roofing_extruder_nr", 241 "top_bottom_extruder_nr", 242 "infill_extruder_nr", 243 ] 244 for extruder_nr_feature_name in limit_to_extruder_feature_list: 245 extruder_nr = int(global_stack.getProperty(extruder_nr_feature_name, "value")) 246 if extruder_nr == -1: 247 continue 248 if str(extruder_nr) not in self.extruderIds: 249 extruder_nr = int(self._application.getMachineManager().defaultExtruderPosition) 250 used_extruder_stack_ids.add(self.extruderIds[str(extruder_nr)]) 251 252 # Check support extruders 253 if support_enabled: 254 used_extruder_stack_ids.add(self.extruderIds[self.extruderValueWithDefault(str(global_stack.getProperty("support_infill_extruder_nr", "value")))]) 255 used_extruder_stack_ids.add(self.extruderIds[self.extruderValueWithDefault(str(global_stack.getProperty("support_extruder_nr_layer_0", "value")))]) 256 if support_bottom_enabled: 257 used_extruder_stack_ids.add(self.extruderIds[self.extruderValueWithDefault(str(global_stack.getProperty("support_bottom_extruder_nr", "value")))]) 258 if support_roof_enabled: 259 used_extruder_stack_ids.add(self.extruderIds[self.extruderValueWithDefault(str(global_stack.getProperty("support_roof_extruder_nr", "value")))]) 260 261 # The platform adhesion extruder. Not used if using none. 262 if global_stack.getProperty("adhesion_type", "value") != "none" or ( 263 global_stack.getProperty("prime_tower_brim_enable", "value") and 264 global_stack.getProperty("adhesion_type", "value") != 'raft'): 265 extruder_str_nr = str(global_stack.getProperty("adhesion_extruder_nr", "value")) 266 if extruder_str_nr == "-1": 267 extruder_str_nr = self._application.getMachineManager().defaultExtruderPosition 268 if extruder_str_nr in self.extruderIds: 269 used_extruder_stack_ids.add(self.extruderIds[extruder_str_nr]) 270 271 try: 272 return [container_registry.findContainerStacks(id = stack_id)[0] for stack_id in used_extruder_stack_ids] 273 except IndexError: # One or more of the extruders was not found. 274 Logger.log("e", "Unable to find one or more of the extruders in %s", used_extruder_stack_ids) 275 return [] 276 277 def getInitialExtruderNr(self) -> int: 278 """Get the extruder that the print will start with. 279 280 This should mirror the implementation in CuraEngine of 281 ``FffGcodeWriter::getStartExtruder()``. 282 """ 283 284 application = cura.CuraApplication.CuraApplication.getInstance() 285 global_stack = application.getGlobalContainerStack() 286 287 # Starts with the adhesion extruder. 288 if global_stack.getProperty("adhesion_type", "value") != "none": 289 return global_stack.getProperty("adhesion_extruder_nr", "value") 290 291 # No adhesion? Well maybe there is still support brim. 292 if (global_stack.getProperty("support_enable", "value") or global_stack.getProperty("support_structure", "value") == "tree") and global_stack.getProperty("support_brim_enable", "value"): 293 return global_stack.getProperty("support_infill_extruder_nr", "value") 294 295 # REALLY no adhesion? Use the first used extruder. 296 return self.getUsedExtruderStacks()[0].getProperty("extruder_nr", "value") 297 298 def removeMachineExtruders(self, machine_id: str) -> None: 299 """Removes the container stack and user profile for the extruders for a specific machine. 300 301 :param machine_id: The machine to remove the extruders for. 302 """ 303 304 for extruder in self.getMachineExtruders(machine_id): 305 ContainerRegistry.getInstance().removeContainer(extruder.userChanges.getId()) 306 ContainerRegistry.getInstance().removeContainer(extruder.definitionChanges.getId()) 307 ContainerRegistry.getInstance().removeContainer(extruder.getId()) 308 if machine_id in self._extruder_trains: 309 del self._extruder_trains[machine_id] 310 311 def getMachineExtruders(self, machine_id: str) -> List["ExtruderStack"]: 312 """Returns extruders for a specific machine. 313 314 :param machine_id: The machine to get the extruders of. 315 """ 316 317 if machine_id not in self._extruder_trains: 318 return [] 319 return [self._extruder_trains[machine_id][name] for name in self._extruder_trains[machine_id]] 320 321 def getActiveExtruderStacks(self) -> List["ExtruderStack"]: 322 """Returns the list of active extruder stacks, taking into account the machine extruder count. 323 324 :return: :type{List[ContainerStack]} a list of 325 """ 326 327 global_stack = self._application.getGlobalContainerStack() 328 if not global_stack: 329 return [] 330 return global_stack.extruderList 331 332 def _globalContainerStackChanged(self) -> None: 333 # If the global container changed, the machine changed and might have extruders that were not registered yet 334 self._addCurrentMachineExtruders() 335 336 self.resetSelectedObjectExtruders() 337 338 def addMachineExtruders(self, global_stack: GlobalStack) -> None: 339 """Adds the extruders to the selected machine.""" 340 341 extruders_changed = False 342 container_registry = ContainerRegistry.getInstance() 343 global_stack_id = global_stack.getId() 344 345 # Gets the extruder trains that we just created as well as any that still existed. 346 extruder_trains = container_registry.findContainerStacks(type = "extruder_train", machine = global_stack_id) 347 348 # Make sure the extruder trains for the new machine can be placed in the set of sets 349 if global_stack_id not in self._extruder_trains: 350 self._extruder_trains[global_stack_id] = {} 351 extruders_changed = True 352 353 # Register the extruder trains by position 354 for extruder_train in extruder_trains: 355 extruder_position = extruder_train.getMetaDataEntry("position") 356 self._extruder_trains[global_stack_id][extruder_position] = extruder_train 357 358 # regardless of what the next stack is, we have to set it again, because of signal routing. ??? 359 extruder_train.setParent(global_stack) 360 extruder_train.setNextStack(global_stack) 361 extruders_changed = True 362 363 self.fixSingleExtrusionMachineExtruderDefinition(global_stack) 364 if extruders_changed: 365 self.extrudersChanged.emit(global_stack_id) 366 367 # After 3.4, all single-extrusion machines have their own extruder definition files instead of reusing 368 # "fdmextruder". We need to check a machine here so its extruder definition is correct according to this. 369 def fixSingleExtrusionMachineExtruderDefinition(self, global_stack: "GlobalStack") -> None: 370 container_registry = ContainerRegistry.getInstance() 371 expected_extruder_definition_0_id = global_stack.getMetaDataEntry("machine_extruder_trains")["0"] 372 try: 373 extruder_stack_0 = global_stack.extruderList[0] 374 except IndexError: 375 extruder_stack_0 = None 376 377 # At this point, extruder stacks for this machine may not have been loaded yet. In this case, need to look in 378 # the container registry as well. 379 if not global_stack.extruderList: 380 extruder_trains = container_registry.findContainerStacks(type = "extruder_train", 381 machine = global_stack.getId()) 382 if extruder_trains: 383 for extruder in extruder_trains: 384 if extruder.getMetaDataEntry("position") == "0": 385 extruder_stack_0 = extruder 386 break 387 388 if extruder_stack_0 is None: 389 Logger.log("i", "No extruder stack for global stack [%s], create one", global_stack.getId()) 390 # Single extrusion machine without an ExtruderStack, create it 391 from cura.Settings.CuraStackBuilder import CuraStackBuilder 392 CuraStackBuilder.createExtruderStackWithDefaultSetup(global_stack, 0) 393 394 elif extruder_stack_0.definition.getId() != expected_extruder_definition_0_id: 395 Logger.log("e", "Single extruder printer [{printer}] expected extruder [{expected}], but got [{got}]. I'm making it [{expected}].".format( 396 printer = global_stack.getId(), expected = expected_extruder_definition_0_id, got = extruder_stack_0.definition.getId())) 397 try: 398 extruder_definition = container_registry.findDefinitionContainers(id = expected_extruder_definition_0_id)[0] 399 except IndexError: 400 # It still needs to break, but we want to know what extruder ID made it break. 401 msg = "Unable to find extruder definition with the id [%s]" % expected_extruder_definition_0_id 402 Logger.logException("e", msg) 403 raise IndexError(msg) 404 extruder_stack_0.definition = extruder_definition 405 406 @pyqtSlot(str, result="QVariant") 407 def getInstanceExtruderValues(self, key: str) -> List: 408 """Get all extruder values for a certain setting. 409 410 This is exposed to qml for display purposes 411 412 :param key: The key of the setting to retrieve values for. 413 414 :return: String representing the extruder values 415 """ 416 417 return self._application.getCuraFormulaFunctions().getValuesInAllExtruders(key) 418 419 @staticmethod 420 def getResolveOrValue(key: str) -> Any: 421 """Get the resolve value or value for a given key 422 423 This is the effective value for a given key, it is used for values in the global stack. 424 This is exposed to SettingFunction to use in value functions. 425 :param key: The key of the setting to get the value of. 426 427 :return: The effective value 428 """ 429 430 global_stack = cast(GlobalStack, cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()) 431 resolved_value = global_stack.getProperty(key, "value") 432 433 return resolved_value 434 435 __instance = None # type: ExtruderManager 436 437 @classmethod 438 def getInstance(cls, *args, **kwargs) -> "ExtruderManager": 439 return cls.__instance 440