SupportEraser.py 7.3 KB

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