SupportEraser.py 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
  1. # Copyright (c) 2018 Ultimaker B.V.
  2. # Cura is released under the terms of the LGPLv3 or higher.
  3. from PyQt6.QtCore import Qt, QTimer
  4. from PyQt6.QtWidgets import QApplication
  5. from UM.Application import Application
  6. from UM.Math.Vector import Vector
  7. from UM.Tool import Tool
  8. from UM.Event import Event, MouseEvent
  9. from UM.Mesh.MeshBuilder import MeshBuilder
  10. from UM.Scene.Selection import Selection
  11. from cura.CuraApplication import CuraApplication
  12. from cura.Scene.CuraSceneNode import CuraSceneNode
  13. from cura.PickingPass import PickingPass
  14. from UM.Operations.GroupedOperation import GroupedOperation
  15. from UM.Operations.AddSceneNodeOperation import AddSceneNodeOperation
  16. from UM.Operations.RemoveSceneNodeOperation import RemoveSceneNodeOperation
  17. from cura.Operations.SetParentOperation import SetParentOperation
  18. from cura.Scene.SliceableObjectDecorator import SliceableObjectDecorator
  19. from cura.Scene.BuildPlateDecorator import BuildPlateDecorator
  20. from UM.Settings.SettingInstance import SettingInstance
  21. import numpy
  22. class SupportEraser(Tool):
  23. def __init__(self):
  24. super().__init__()
  25. self._shortcut_key = Qt.Key.Key_E
  26. self._controller = self.getController()
  27. self._selection_pass = None
  28. CuraApplication.getInstance().globalContainerStackChanged.connect(self._updateEnabled)
  29. # Note: if the selection is cleared with this tool active, there is no way to switch to
  30. # another tool than to reselect an object (by clicking it) because the tool buttons in the
  31. # toolbar will have been disabled. That is why we need to ignore the first press event
  32. # after the selection has been cleared.
  33. Selection.selectionChanged.connect(self._onSelectionChanged)
  34. self._had_selection = False
  35. self._skip_press = False
  36. self._had_selection_timer = QTimer()
  37. self._had_selection_timer.setInterval(0)
  38. self._had_selection_timer.setSingleShot(True)
  39. self._had_selection_timer.timeout.connect(self._selectionChangeDelay)
  40. def event(self, event):
  41. super().event(event)
  42. modifiers = QApplication.keyboardModifiers()
  43. ctrl_is_active = modifiers & Qt.ControlModifier
  44. if event.type == Event.MousePressEvent and MouseEvent.LeftButton in event.buttons and self._controller.getToolsEnabled():
  45. if ctrl_is_active:
  46. self._controller.setActiveTool("TranslateTool")
  47. return
  48. if self._skip_press:
  49. # The selection was previously cleared, do not add/remove an anti-support mesh but
  50. # use this click for selection and reactivating this tool only.
  51. self._skip_press = False
  52. return
  53. if self._selection_pass is None:
  54. # The selection renderpass is used to identify objects in the current view
  55. self._selection_pass = Application.getInstance().getRenderer().getRenderPass("selection")
  56. picked_node = self._controller.getScene().findObject(self._selection_pass.getIdAtPosition(event.x, event.y))
  57. if not picked_node:
  58. # There is no slicable object at the picked location
  59. return
  60. node_stack = picked_node.callDecoration("getStack")
  61. if node_stack:
  62. if node_stack.getProperty("anti_overhang_mesh", "value"):
  63. self._removeEraserMesh(picked_node)
  64. return
  65. elif node_stack.getProperty("support_mesh", "value") or node_stack.getProperty("infill_mesh", "value") or node_stack.getProperty("cutting_mesh", "value"):
  66. # Only "normal" meshes can have anti_overhang_meshes added to them
  67. return
  68. # Create a pass for picking a world-space location from the mouse location
  69. active_camera = self._controller.getScene().getActiveCamera()
  70. picking_pass = PickingPass(active_camera.getViewportWidth(), active_camera.getViewportHeight())
  71. picking_pass.render()
  72. picked_position = picking_pass.getPickedPosition(event.x, event.y)
  73. # Add the anti_overhang_mesh cube at the picked location
  74. self._createEraserMesh(picked_node, picked_position)
  75. def _createEraserMesh(self, parent: CuraSceneNode, position: Vector):
  76. node = CuraSceneNode()
  77. node.setName("Eraser")
  78. node.setSelectable(True)
  79. node.setCalculateBoundingBox(True)
  80. mesh = self._createCube(10)
  81. node.setMeshData(mesh.build())
  82. node.calculateBoundingBoxMesh()
  83. active_build_plate = CuraApplication.getInstance().getMultiBuildPlateModel().activeBuildPlate
  84. node.addDecorator(BuildPlateDecorator(active_build_plate))
  85. node.addDecorator(SliceableObjectDecorator())
  86. stack = node.callDecoration("getStack") # created by SettingOverrideDecorator that is automatically added to CuraSceneNode
  87. settings = stack.getTop()
  88. definition = stack.getSettingDefinition("anti_overhang_mesh")
  89. new_instance = SettingInstance(definition, settings)
  90. new_instance.setProperty("value", True)
  91. new_instance.resetState() # Ensure that the state is not seen as a user state.
  92. settings.addInstance(new_instance)
  93. op = GroupedOperation()
  94. # First add node to the scene at the correct position/scale, before parenting, so the eraser mesh does not get scaled with the parent
  95. op.addOperation(AddSceneNodeOperation(node, self._controller.getScene().getRoot()))
  96. op.addOperation(SetParentOperation(node, parent))
  97. op.push()
  98. node.setPosition(position, CuraSceneNode.TransformSpace.World)
  99. CuraApplication.getInstance().getController().getScene().sceneChanged.emit(node)
  100. def _removeEraserMesh(self, node: CuraSceneNode):
  101. parent = node.getParent()
  102. if parent == self._controller.getScene().getRoot():
  103. parent = None
  104. op = RemoveSceneNodeOperation(node)
  105. op.push()
  106. if parent and not Selection.isSelected(parent):
  107. Selection.add(parent)
  108. CuraApplication.getInstance().getController().getScene().sceneChanged.emit(node)
  109. def _updateEnabled(self):
  110. plugin_enabled = False
  111. global_container_stack = CuraApplication.getInstance().getGlobalContainerStack()
  112. if global_container_stack:
  113. plugin_enabled = global_container_stack.getProperty("anti_overhang_mesh", "enabled")
  114. CuraApplication.getInstance().getController().toolEnabledChanged.emit(self._plugin_id, plugin_enabled)
  115. def _onSelectionChanged(self):
  116. # When selection is passed from one object to another object, first the selection is cleared
  117. # and then it is set to the new object. We are only interested in the change from no selection
  118. # to a selection or vice-versa, not in a change from one object to another. A timer is used to
  119. # "merge" a possible clear/select action in a single frame
  120. if Selection.hasSelection() != self._had_selection:
  121. self._had_selection_timer.start()
  122. def _selectionChangeDelay(self):
  123. has_selection = Selection.hasSelection()
  124. if not has_selection and self._had_selection:
  125. self._skip_press = True
  126. else:
  127. self._skip_press = False
  128. self._had_selection = has_selection
  129. def _createCube(self, size):
  130. mesh = MeshBuilder()
  131. # Can't use MeshBuilder.addCube() because that does not get per-vertex normals
  132. # Per-vertex normals require duplication of vertices
  133. s = size / 2
  134. verts = [ # 6 faces with 4 corners each
  135. [-s, -s, s], [-s, s, s], [ s, s, s], [ s, -s, s],
  136. [-s, s, -s], [-s, -s, -s], [ s, -s, -s], [ s, s, -s],
  137. [ s, -s, -s], [-s, -s, -s], [-s, -s, s], [ s, -s, s],
  138. [-s, s, -s], [ s, s, -s], [ s, s, s], [-s, s, s],
  139. [-s, -s, s], [-s, -s, -s], [-s, s, -s], [-s, s, s],
  140. [ s, -s, -s], [ s, -s, s], [ s, s, s], [ s, s, -s]
  141. ]
  142. mesh.setVertices(numpy.asarray(verts, dtype=numpy.float32))
  143. indices = []
  144. for i in range(0, 24, 4): # All 6 quads (12 triangles)
  145. indices.append([i, i+2, i+1])
  146. indices.append([i, i+3, i+2])
  147. mesh.setIndices(numpy.asarray(indices, dtype=numpy.int32))
  148. mesh.calculateNormals()
  149. return mesh