123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190 |
- # Copyright (c) 2018 Ultimaker B.V.
- # Cura is released under the terms of the LGPLv3 or higher.
- from PyQt6.QtCore import Qt, QTimer
- from PyQt6.QtWidgets import QApplication
- from UM.Application import Application
- from UM.Math.Vector import Vector
- from UM.Tool import Tool
- from UM.Event import Event, MouseEvent
- from UM.Mesh.MeshBuilder import MeshBuilder
- from UM.Scene.Selection import Selection
- from cura.CuraApplication import CuraApplication
- from cura.Scene.CuraSceneNode import CuraSceneNode
- from cura.PickingPass import PickingPass
- from UM.Operations.GroupedOperation import GroupedOperation
- from UM.Operations.AddSceneNodeOperation import AddSceneNodeOperation
- from UM.Operations.RemoveSceneNodeOperation import RemoveSceneNodeOperation
- from cura.Operations.SetParentOperation import SetParentOperation
- from cura.Scene.SliceableObjectDecorator import SliceableObjectDecorator
- from cura.Scene.BuildPlateDecorator import BuildPlateDecorator
- from UM.Settings.SettingInstance import SettingInstance
- import numpy
- class SupportEraser(Tool):
- def __init__(self):
- super().__init__()
- self._shortcut_key = Qt.Key.Key_E
- self._controller = self.getController()
- self._selection_pass = None
- CuraApplication.getInstance().globalContainerStackChanged.connect(self._updateEnabled)
- # Note: if the selection is cleared with this tool active, there is no way to switch to
- # another tool than to reselect an object (by clicking it) because the tool buttons in the
- # toolbar will have been disabled. That is why we need to ignore the first press event
- # after the selection has been cleared.
- Selection.selectionChanged.connect(self._onSelectionChanged)
- self._had_selection = False
- self._skip_press = False
- self._had_selection_timer = QTimer()
- self._had_selection_timer.setInterval(0)
- self._had_selection_timer.setSingleShot(True)
- self._had_selection_timer.timeout.connect(self._selectionChangeDelay)
- def event(self, event):
- super().event(event)
- modifiers = QApplication.keyboardModifiers()
- ctrl_is_active = modifiers & Qt.ControlModifier
- if event.type == Event.MousePressEvent and MouseEvent.LeftButton in event.buttons and self._controller.getToolsEnabled():
- if ctrl_is_active:
- self._controller.setActiveTool("TranslateTool")
- return
- if self._skip_press:
- # The selection was previously cleared, do not add/remove an anti-support mesh but
- # use this click for selection and reactivating this tool only.
- self._skip_press = False
- return
- if self._selection_pass is None:
- # The selection renderpass is used to identify objects in the current view
- self._selection_pass = Application.getInstance().getRenderer().getRenderPass("selection")
- picked_node = self._controller.getScene().findObject(self._selection_pass.getIdAtPosition(event.x, event.y))
- if not picked_node:
- # There is no slicable object at the picked location
- return
- node_stack = picked_node.callDecoration("getStack")
- if node_stack:
- if node_stack.getProperty("anti_overhang_mesh", "value"):
- self._removeEraserMesh(picked_node)
- return
- elif node_stack.getProperty("support_mesh", "value") or node_stack.getProperty("infill_mesh", "value") or node_stack.getProperty("cutting_mesh", "value"):
- # Only "normal" meshes can have anti_overhang_meshes added to them
- return
- # Create a pass for picking a world-space location from the mouse location
- active_camera = self._controller.getScene().getActiveCamera()
- picking_pass = PickingPass(active_camera.getViewportWidth(), active_camera.getViewportHeight())
- picking_pass.render()
- picked_position = picking_pass.getPickedPosition(event.x, event.y)
- # Add the anti_overhang_mesh cube at the picked location
- self._createEraserMesh(picked_node, picked_position)
- def _createEraserMesh(self, parent: CuraSceneNode, position: Vector):
- node = CuraSceneNode()
- node.setName("Eraser")
- node.setSelectable(True)
- node.setCalculateBoundingBox(True)
- mesh = self._createCube(10)
- node.setMeshData(mesh.build())
- node.calculateBoundingBoxMesh()
- active_build_plate = CuraApplication.getInstance().getMultiBuildPlateModel().activeBuildPlate
- node.addDecorator(BuildPlateDecorator(active_build_plate))
- node.addDecorator(SliceableObjectDecorator())
- stack = node.callDecoration("getStack") # created by SettingOverrideDecorator that is automatically added to CuraSceneNode
- settings = stack.getTop()
- definition = stack.getSettingDefinition("anti_overhang_mesh")
- new_instance = SettingInstance(definition, settings)
- new_instance.setProperty("value", True)
- new_instance.resetState() # Ensure that the state is not seen as a user state.
- settings.addInstance(new_instance)
- op = GroupedOperation()
- # First add node to the scene at the correct position/scale, before parenting, so the eraser mesh does not get scaled with the parent
- op.addOperation(AddSceneNodeOperation(node, self._controller.getScene().getRoot()))
- op.addOperation(SetParentOperation(node, parent))
- op.push()
- node.setPosition(position, CuraSceneNode.TransformSpace.World)
- CuraApplication.getInstance().getController().getScene().sceneChanged.emit(node)
- def _removeEraserMesh(self, node: CuraSceneNode):
- parent = node.getParent()
- if parent == self._controller.getScene().getRoot():
- parent = None
- op = RemoveSceneNodeOperation(node)
- op.push()
- if parent and not Selection.isSelected(parent):
- Selection.add(parent)
- CuraApplication.getInstance().getController().getScene().sceneChanged.emit(node)
- def _updateEnabled(self):
- plugin_enabled = False
- global_container_stack = CuraApplication.getInstance().getGlobalContainerStack()
- if global_container_stack:
- plugin_enabled = global_container_stack.getProperty("anti_overhang_mesh", "enabled")
- CuraApplication.getInstance().getController().toolEnabledChanged.emit(self._plugin_id, plugin_enabled)
- def _onSelectionChanged(self):
- # When selection is passed from one object to another object, first the selection is cleared
- # and then it is set to the new object. We are only interested in the change from no selection
- # to a selection or vice-versa, not in a change from one object to another. A timer is used to
- # "merge" a possible clear/select action in a single frame
- if Selection.hasSelection() != self._had_selection:
- self._had_selection_timer.start()
- def _selectionChangeDelay(self):
- has_selection = Selection.hasSelection()
- if not has_selection and self._had_selection:
- self._skip_press = True
- else:
- self._skip_press = False
- self._had_selection = has_selection
- def _createCube(self, size):
- mesh = MeshBuilder()
- # Can't use MeshBuilder.addCube() because that does not get per-vertex normals
- # Per-vertex normals require duplication of vertices
- s = size / 2
- verts = [ # 6 faces with 4 corners each
- [-s, -s, s], [-s, s, s], [ s, s, s], [ s, -s, s],
- [-s, s, -s], [-s, -s, -s], [ s, -s, -s], [ s, s, -s],
- [ s, -s, -s], [-s, -s, -s], [-s, -s, s], [ s, -s, s],
- [-s, s, -s], [ s, s, -s], [ s, s, s], [-s, s, s],
- [-s, -s, s], [-s, -s, -s], [-s, s, -s], [-s, s, s],
- [ s, -s, -s], [ s, -s, s], [ s, s, s], [ s, s, -s]
- ]
- mesh.setVertices(numpy.asarray(verts, dtype=numpy.float32))
- indices = []
- for i in range(0, 24, 4): # All 6 quads (12 triangles)
- indices.append([i, i+2, i+1])
- indices.append([i, i+3, i+2])
- mesh.setIndices(numpy.asarray(indices, dtype=numpy.int32))
- mesh.calculateNormals()
- return mesh
|