mirror of
https://github.com/Ultimaker/Cura.git
synced 2025-12-25 08:58:35 -07:00
CURA-12660 When a message box is displayed, some offscreen rendering passes (face selection) render an unpredictable result and we are unable to start painting. This went through a refactoring of the rendering passes. Since doing the offscreen rendering outside the Qt rendering loop caused some troubles, we now use the rendering passes only inside the Qt rendering loop, so that they work properly. Tools also have the ability to indicate which extra passes they require, so that we don't run all the passes when they are not required. Since this issue also concerns the support blockers placement and rotation by face selection, they have been updated so that they now also always work. The face selection mechanism using the Selection class was partially working and used only by the rotation, so now it has been deprecated in favor of the new mechanism.
198 lines
No EOL
8.5 KiB
Python
198 lines
No EOL
8.5 KiB
Python
# Copyright (c) 2018 Ultimaker B.V.
|
|
# Cura is released under the terms of the LGPLv3 or higher.
|
|
|
|
from typing import Optional
|
|
|
|
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
|
|
self._picking_pass: Optional[PickingPass] = 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
|
|
|
|
# Get the pass for picking a world-space location from the mouse location
|
|
if self._picking_pass is None:
|
|
self._picking_pass = Application.getInstance().getRenderer().getRenderPass("picking_selected")
|
|
if not self._picking_pass:
|
|
return
|
|
|
|
picked_position = self._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
|
|
|
|
def getRequiredExtraRenderingPasses(self) -> list[str]:
|
|
return ["picking_selected"] |