1# Copyright (c) 2018 Ultimaker B.V. 2# Cura is released under the terms of the LGPLv3 or higher. 3 4from PyQt5.QtCore import QObject, QUrl 5from PyQt5.QtGui import QDesktopServices 6from typing import List, cast 7 8from UM.Event import CallFunctionEvent 9from UM.FlameProfiler import pyqtSlot 10from UM.Math.Vector import Vector 11from UM.Scene.Selection import Selection 12from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator 13from UM.Operations.GroupedOperation import GroupedOperation 14from UM.Operations.RemoveSceneNodeOperation import RemoveSceneNodeOperation 15from UM.Operations.TranslateOperation import TranslateOperation 16 17import cura.CuraApplication 18from cura.Operations.SetParentOperation import SetParentOperation 19from cura.MultiplyObjectsJob import MultiplyObjectsJob 20from cura.Settings.SetObjectExtruderOperation import SetObjectExtruderOperation 21from cura.Settings.ExtruderManager import ExtruderManager 22 23from cura.Operations.SetBuildPlateNumberOperation import SetBuildPlateNumberOperation 24 25from UM.Logger import Logger 26from UM.Scene.SceneNode import SceneNode 27 28 29class CuraActions(QObject): 30 def __init__(self, parent: QObject = None) -> None: 31 super().__init__(parent) 32 33 @pyqtSlot() 34 def openDocumentation(self) -> None: 35 # Starting a web browser from a signal handler connected to a menu will crash on windows. 36 # So instead, defer the call to the next run of the event loop, since that does work. 37 # Note that weirdly enough, only signal handlers that open a web browser fail like that. 38 event = CallFunctionEvent(self._openUrl, [QUrl("https://ultimaker.com/en/resources/manuals/software")], {}) 39 cura.CuraApplication.CuraApplication.getInstance().functionEvent(event) 40 41 @pyqtSlot() 42 def openBugReportPage(self) -> None: 43 event = CallFunctionEvent(self._openUrl, [QUrl("https://github.com/Ultimaker/Cura/issues/new/choose")], {}) 44 cura.CuraApplication.CuraApplication.getInstance().functionEvent(event) 45 46 @pyqtSlot() 47 def homeCamera(self) -> None: 48 """Reset camera position and direction to default""" 49 50 scene = cura.CuraApplication.CuraApplication.getInstance().getController().getScene() 51 camera = scene.getActiveCamera() 52 if camera: 53 diagonal_size = cura.CuraApplication.CuraApplication.getInstance().getBuildVolume().getDiagonalSize() 54 camera.setPosition(Vector(-80, 250, 700) * diagonal_size / 375) 55 camera.setPerspective(True) 56 camera.lookAt(Vector(0, 0, 0)) 57 58 @pyqtSlot() 59 def centerSelection(self) -> None: 60 """Center all objects in the selection""" 61 62 operation = GroupedOperation() 63 for node in Selection.getAllSelectedObjects(): 64 current_node = node 65 parent_node = current_node.getParent() 66 while parent_node and parent_node.callDecoration("isGroup"): 67 current_node = parent_node 68 parent_node = current_node.getParent() 69 70 # This was formerly done with SetTransformOperation but because of 71 # unpredictable matrix deconstruction it was possible that mirrors 72 # could manifest as rotations. Centering is therefore done by 73 # moving the node to negative whatever its position is: 74 center_operation = TranslateOperation(current_node, -current_node._position) 75 operation.addOperation(center_operation) 76 operation.push() 77 78 @pyqtSlot(int) 79 def multiplySelection(self, count: int) -> None: 80 """Multiply all objects in the selection 81 82 :param count: The number of times to multiply the selection. 83 """ 84 85 min_offset = cura.CuraApplication.CuraApplication.getInstance().getBuildVolume().getEdgeDisallowedSize() + 2 # Allow for some rounding errors 86 job = MultiplyObjectsJob(Selection.getAllSelectedObjects(), count, min_offset = max(min_offset, 8)) 87 job.start() 88 89 @pyqtSlot() 90 def deleteSelection(self) -> None: 91 """Delete all selected objects.""" 92 93 if not cura.CuraApplication.CuraApplication.getInstance().getController().getToolsEnabled(): 94 return 95 96 removed_group_nodes = [] #type: List[SceneNode] 97 op = GroupedOperation() 98 nodes = Selection.getAllSelectedObjects() 99 for node in nodes: 100 op.addOperation(RemoveSceneNodeOperation(node)) 101 group_node = node.getParent() 102 if group_node and group_node.callDecoration("isGroup") and group_node not in removed_group_nodes: 103 remaining_nodes_in_group = list(set(group_node.getChildren()) - set(nodes)) 104 if len(remaining_nodes_in_group) == 1: 105 removed_group_nodes.append(group_node) 106 op.addOperation(SetParentOperation(remaining_nodes_in_group[0], group_node.getParent())) 107 op.addOperation(RemoveSceneNodeOperation(group_node)) 108 109 # Reset the print information 110 cura.CuraApplication.CuraApplication.getInstance().getController().getScene().sceneChanged.emit(node) 111 112 op.push() 113 114 @pyqtSlot(str) 115 def setExtruderForSelection(self, extruder_id: str) -> None: 116 """Set the extruder that should be used to print the selection. 117 118 :param extruder_id: The ID of the extruder stack to use for the selected objects. 119 """ 120 121 operation = GroupedOperation() 122 123 nodes_to_change = [] 124 for node in Selection.getAllSelectedObjects(): 125 # If the node is a group, apply the active extruder to all children of the group. 126 if node.callDecoration("isGroup"): 127 for grouped_node in BreadthFirstIterator(node): #type: ignore #Ignore type error because iter() should get called automatically by Python syntax. 128 if grouped_node.callDecoration("getActiveExtruder") == extruder_id: 129 continue 130 131 if grouped_node.callDecoration("isGroup"): 132 continue 133 134 nodes_to_change.append(grouped_node) 135 continue 136 137 # Do not change any nodes that already have the right extruder set. 138 if node.callDecoration("getActiveExtruder") == extruder_id: 139 continue 140 141 nodes_to_change.append(node) 142 143 if not nodes_to_change: 144 # If there are no changes to make, we still need to reset the selected extruders. 145 # This is a workaround for checked menu items being deselected while still being 146 # selected. 147 ExtruderManager.getInstance().resetSelectedObjectExtruders() 148 return 149 150 for node in nodes_to_change: 151 operation.addOperation(SetObjectExtruderOperation(node, extruder_id)) 152 operation.push() 153 154 @pyqtSlot(int) 155 def setBuildPlateForSelection(self, build_plate_nr: int) -> None: 156 Logger.log("d", "Setting build plate number... %d" % build_plate_nr) 157 operation = GroupedOperation() 158 159 root = cura.CuraApplication.CuraApplication.getInstance().getController().getScene().getRoot() 160 161 nodes_to_change = [] # type: List[SceneNode] 162 for node in Selection.getAllSelectedObjects(): 163 parent_node = node # Find the parent node to change instead 164 while parent_node.getParent() != root: 165 parent_node = cast(SceneNode, parent_node.getParent()) 166 167 for single_node in BreadthFirstIterator(parent_node): # type: ignore #Ignore type error because iter() should get called automatically by Python syntax. 168 nodes_to_change.append(single_node) 169 170 if not nodes_to_change: 171 Logger.log("d", "Nothing to change.") 172 return 173 174 for node in nodes_to_change: 175 operation.addOperation(SetBuildPlateNumberOperation(node, build_plate_nr)) 176 operation.push() 177 178 Selection.clear() 179 180 def _openUrl(self, url: QUrl) -> None: 181 QDesktopServices.openUrl(url) 182