Fix unability to paint with visible message box

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.
This commit is contained in:
Erwan MATHIEU 2025-08-01 13:10:03 +02:00
parent 3cb7eb3c87
commit ab58dec5d1
6 changed files with 91 additions and 23 deletions

View file

@ -60,6 +60,7 @@ from cura import ApplicationMetadata
from cura.API import CuraAPI
from cura.API.Account import Account
from cura.Arranging.ArrangeObjectsJob import ArrangeObjectsJob
from cura.CuraRenderer import CuraRenderer
from cura.Machines.MachineErrorChecker import MachineErrorChecker
from cura.Machines.Models.BuildPlateModel import BuildPlateModel
from cura.Machines.Models.CustomQualityProfilesDropDownMenuModel import CustomQualityProfilesDropDownMenuModel
@ -362,6 +363,9 @@ class CuraApplication(QtApplication):
self._machine_action_manager = MachineActionManager(self)
self._machine_action_manager.initialize()
def makeRenderer(self) -> CuraRenderer:
return CuraRenderer(self)
def __sendCommandToSingleInstance(self):
self._single_instance = SingleInstance(self, self._files_to_open, self._urls_to_open)

46
cura/CuraRenderer.py Normal file
View file

@ -0,0 +1,46 @@
# Copyright (c) 2025 UltiMaker
# Uranium is released under the terms of the LGPLv3 or higher.
from typing import TYPE_CHECKING
from cura.PickingPass import PickingPass
from UM.Qt.QtRenderer import QtRenderer
from UM.View.RenderPass import RenderPass
from UM.View.SelectionPass import SelectionPass
if TYPE_CHECKING:
from cura.CuraApplication import CuraApplication
class CuraRenderer(QtRenderer):
"""An overridden Renderer implementation that adds some behaviors specific to Cura."""
def __init__(self, application: "CuraApplication") -> None:
super().__init__()
self._controller = application.getController()
self._controller.activeToolChanged.connect(self._onActiveToolChanged)
self._extra_rendering_passes: list[RenderPass] = []
def _onActiveToolChanged(self) -> None:
tool_extra_rendering_passes = []
active_tool = self._controller.getActiveTool()
if active_tool is not None:
tool_extra_rendering_passes = active_tool.getRequiredExtraRenderingPasses()
for extra_rendering_pass in self._extra_rendering_passes:
extra_rendering_pass.setEnabled(extra_rendering_pass.getName() in tool_extra_rendering_passes)
def _makeRenderPasses(self) -> list[RenderPass]:
self._extra_rendering_passes = [
SelectionPass(self._viewport_width, self._viewport_height, SelectionPass.SelectionMode.FACES),
PickingPass(self._viewport_width, self._viewport_height, only_selected_objects=True),
PickingPass(self._viewport_width, self._viewport_height, only_selected_objects=False)
]
for extra_rendering_pass in self._extra_rendering_passes:
extra_rendering_pass.setEnabled(False)
return super()._makeRenderPasses() + self._extra_rendering_passes

View file

@ -29,7 +29,7 @@ class PickingPass(RenderPass):
"""
def __init__(self, width: int, height: int, only_selected_objects: bool = False) -> None:
super().__init__("picking", width, height)
super().__init__("picking" if not only_selected_objects else "picking_selected", width, height)
self._renderer = QtApplication.getInstance().getRenderer()

View file

@ -17,7 +17,9 @@ from UM.Scene.SceneNode import SceneNode
from UM.Scene.Selection import Selection
from UM.Tool import Tool
from cura.CuraApplication import CuraApplication
from cura.PickingPass import PickingPass
from UM.View.SelectionPass import SelectionPass
from .PaintView import PaintView
@ -34,6 +36,7 @@ class PaintTool(Tool):
super().__init__()
self._picking_pass: Optional[PickingPass] = None
self._faces_selection_pass: Optional[SelectionPass] = None
self._shortcut_key: Qt.Key = Qt.Key.Key_P
@ -52,6 +55,8 @@ class PaintTool(Tool):
self._last_mouse_coords: Optional[Tuple[int, int]] = None
self._last_face_id: Optional[int] = None
Selection.selectionChanged.connect(self._updateIgnoreUnselectedObjects)
def _createBrushPen(self) -> QPen:
pen = QPen()
pen.setWidth(self._brush_size)
@ -179,7 +184,7 @@ class PaintTool(Tool):
self._cache_dirty = True
def _getTexCoordsFromClick(self, node: SceneNode, x: float, y: float) -> Tuple[int, Optional[numpy.ndarray]]:
face_id = self._selection_pass.getFaceIdAtPosition(x, y)
face_id = self._faces_selection_pass.getFaceIdAtPosition(x, y)
if face_id < 0 or face_id >= node.getMeshData().getFaceCount():
return face_id, None
@ -248,11 +253,14 @@ class PaintTool(Tool):
if event.type == Event.ToolActivateEvent:
controller.setActiveStage("PrepareStage")
controller.setActiveView("PaintTool") # Because that's the plugin-name, and the view is registered to it.
self._updateIgnoreUnselectedObjects()
return True
if event.type == Event.ToolDeactivateEvent:
controller.setActiveStage("PrepareStage")
controller.setActiveView("SolidView")
CuraApplication.getInstance().getRenderer().getRenderPass("selection").setIgnoreUnselectedObjects(False)
CuraApplication.getInstance().getRenderer().getRenderPass("selection_faces").setIgnoreUnselectedObjects(False)
return True
if event.type == Event.MouseReleaseEvent and self._controller.getToolsEnabled():
@ -281,8 +289,15 @@ class PaintTool(Tool):
if paintview is None:
return False
if not self._selection_pass:
return False
if not self._faces_selection_pass:
self._faces_selection_pass = CuraApplication.getInstance().getRenderer().getRenderPass("selection_faces")
if not self._faces_selection_pass:
return False
if not self._picking_pass:
self._picking_pass = CuraApplication.getInstance().getRenderer().getRenderPass("picking_selected")
if not self._picking_pass:
return False
camera = self._controller.getScene().getActiveCamera()
if not camera:
@ -300,14 +315,6 @@ class PaintTool(Tool):
if not self._mesh_transformed_cache:
return False
if not self._picking_pass:
self._picking_pass = PickingPass(camera.getViewportWidth(),
camera.getViewportHeight(),
only_selected_objects = True)
self._picking_pass.render()
self._selection_pass.renderFacesMode()
face_id, texcoords = self._getTexCoordsFromClick(node, mouse_evt.x, mouse_evt.y)
if texcoords is None:
return False
@ -347,4 +354,13 @@ class PaintTool(Tool):
if node is None:
node = Selection.getSelectedObject(0)
if node is not None:
Application.getInstance().getController().getScene().sceneChanged.emit(node)
Application.getInstance().getController().getScene().sceneChanged.emit(node)
def getRequiredExtraRenderingPasses(self) -> list[str]:
return ["selection_faces", "picking_selected"]
def _updateIgnoreUnselectedObjects(self):
if self._controller.getActiveTool() is self:
ignore_unselected_objects = len(Selection.getAllSelectedObjects()) == 1
CuraApplication.getInstance().getRenderer().getRenderPass("selection").setIgnoreUnselectedObjects(ignore_unselected_objects)
CuraApplication.getInstance().getRenderer().getRenderPass("selection_faces").setIgnoreUnselectedObjects(ignore_unselected_objects)

View file

@ -13,7 +13,6 @@ from plugins.SolidView.SolidView import SolidView
from UM.PluginRegistry import PluginRegistry
from UM.View.GL.ShaderProgram import ShaderProgram
from UM.View.GL.Texture import Texture
from UM.View.SelectionPass import SelectionPass
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
from UM.Scene.Selection import Selection
from UM.View.GL.OpenGL import OpenGL
@ -187,10 +186,6 @@ class PaintView(SolidView):
paint_batch = renderer.createRenderBatch(shader=self._paint_shader)
renderer.addRenderBatch(paint_batch)
selection_pass = cast(SelectionPass, renderer.getRenderPass("selection"))
if selection_pass is not None:
selection_pass.setIgnoreUnselectedObjectsDuringNextRender()
for node in display_objects:
paint_batch.addItem(node.getWorldTransformation(copy=False), node.getMeshData(), normal_transformation=node.getCachedNormalMatrix())
self._current_paint_texture = node.callDecoration("getPaintTexture")

View file

@ -1,6 +1,8 @@
# 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
@ -35,6 +37,7 @@ class SupportEraser(Tool):
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
@ -84,12 +87,13 @@ class SupportEraser(Tool):
# 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()
# 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 = picking_pass.getPickedPosition(event.x, event.y)
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)
@ -189,3 +193,6 @@ class SupportEraser(Tool):
mesh.calculateNormals()
return mesh
def getRequiredExtraRenderingPasses(self) -> list[str]:
return ["picking_selected"]