mirror of
https://github.com/Ultimaker/Cura.git
synced 2026-02-15 17:09:33 -07:00
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:
parent
3cb7eb3c87
commit
ab58dec5d1
6 changed files with 91 additions and 23 deletions
|
|
@ -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
46
cura/CuraRenderer.py
Normal 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
|
||||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
Loading…
Add table
Add a link
Reference in a new issue