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