mirror of
https://git.mirrors.martin98.com/https://github.com/Ultimaker/Cura
synced 2025-04-22 13:49:39 +08:00
192 lines
8.3 KiB
Python
192 lines
8.3 KiB
Python
# 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.Operations.TranslateOperation import TranslateOperation
|
|
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.KeyboardModifier.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.addOperation(TranslateOperation(node, position, set_position = True))
|
|
op.push()
|
|
|
|
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
|