mirror of
https://github.com/Ultimaker/Cura.git
synced 2026-01-07 23:27:48 -07:00
Merge remote-tracking branch 'origin/CURA-12660_painting-UI-improvements' into CURA-12662_paint_splatter_issue
This commit is contained in:
commit
4f465bc30f
10 changed files with 184 additions and 81 deletions
|
|
@ -1038,7 +1038,6 @@ class CuraApplication(QtApplication):
|
|||
|
||||
# Initialize UI state
|
||||
controller.setActiveStage("PrepareStage")
|
||||
controller.setActiveView("SolidView")
|
||||
controller.setCameraTool("CameraTool")
|
||||
controller.setSelectionTool("SelectionTool")
|
||||
|
||||
|
|
@ -2089,9 +2088,7 @@ class CuraApplication(QtApplication):
|
|||
is_non_sliceable = "." + file_extension in self._non_sliceable_extensions
|
||||
|
||||
if is_non_sliceable:
|
||||
# Need to switch first to the preview stage and then to layer view
|
||||
self.callLater(lambda: (self.getController().setActiveStage("PreviewStage"),
|
||||
self.getController().setActiveView("SimulationView")))
|
||||
self.callLater(lambda: (self.getController().setActiveStage("PreviewStage")))
|
||||
|
||||
block_slicing_decorator = BlockSlicingDecorator()
|
||||
node.addDecorator(block_slicing_decorator)
|
||||
|
|
|
|||
|
|
@ -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 pyqtProperty, QUrl
|
||||
|
||||
from UM.Stage import Stage
|
||||
|
|
@ -13,8 +15,8 @@ from UM.Stage import Stage
|
|||
# * The MainComponent is the component that will be drawn starting from the bottom of the stageBar and fills the rest
|
||||
# of the screen.
|
||||
class CuraStage(Stage):
|
||||
def __init__(self, parent = None) -> None:
|
||||
super().__init__(parent)
|
||||
def __init__(self, parent = None, active_view: Optional[str] = "SolidView") -> None:
|
||||
super().__init__(parent, active_view = active_view)
|
||||
|
||||
@pyqtProperty(str, constant = True)
|
||||
def stageId(self) -> str:
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ from typing import cast, Optional, Tuple, List
|
|||
|
||||
from UM.Application import Application
|
||||
from UM.Event import Event, MouseEvent
|
||||
from UM.Job import Job
|
||||
from UM.Logger import Logger
|
||||
from UM.Math.Polygon import Polygon
|
||||
from UM.Scene.SceneNode import SceneNode
|
||||
|
|
@ -20,6 +21,7 @@ from cura.CuraApplication import CuraApplication
|
|||
from cura.PickingPass import PickingPass
|
||||
from UM.View.SelectionPass import SelectionPass
|
||||
from .PaintView import PaintView
|
||||
from .PrepareTextureJob import PrepareTextureJob
|
||||
|
||||
|
||||
class PaintTool(Tool):
|
||||
|
|
@ -31,6 +33,13 @@ class PaintTool(Tool):
|
|||
SQUARE = 0
|
||||
CIRCLE = 1
|
||||
|
||||
class Paint(QObject):
|
||||
@pyqtEnum
|
||||
class State(IntEnum):
|
||||
MULTIPLE_SELECTION = 0 # Multiple objects are selected, wait until there is only one
|
||||
PREPARING_MODEL = 1 # Model is being prepared (UV-unwrapping, texture generation)
|
||||
READY = 2 # Ready to paint !
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
|
||||
|
|
@ -54,9 +63,13 @@ class PaintTool(Tool):
|
|||
self._last_mouse_coords: Optional[Tuple[int, int]] = None
|
||||
self._last_face_id: Optional[int] = None
|
||||
|
||||
self.setExposedProperties("PaintType", "BrushSize", "BrushColor", "BrushShape")
|
||||
self._state: PaintTool.Paint.State = PaintTool.Paint.State.MULTIPLE_SELECTION
|
||||
self._prepare_texture_job: Optional[PrepareTextureJob] = None
|
||||
|
||||
Selection.selectionChanged.connect(self._updateIgnoreUnselectedObjects)
|
||||
self.setExposedProperties("PaintType", "BrushSize", "BrushColor", "BrushShape", "State")
|
||||
|
||||
self._controller.activeViewChanged.connect(self._updateIgnoreUnselectedObjects)
|
||||
self._controller.activeToolChanged.connect(self._updateState)
|
||||
|
||||
def _createBrushPen(self) -> QPen:
|
||||
pen = QPen()
|
||||
|
|
@ -139,6 +152,9 @@ class PaintTool(Tool):
|
|||
self._brush_pen = self._createBrushPen()
|
||||
self.propertyChanged.emit()
|
||||
|
||||
def getState(self) -> int:
|
||||
return self._state
|
||||
|
||||
def undoStackAction(self, redo_instead: bool) -> bool:
|
||||
paint_view = self._getPaintView()
|
||||
if paint_view is None:
|
||||
|
|
@ -364,23 +380,6 @@ class PaintTool(Tool):
|
|||
seen.add(candidate)
|
||||
return res
|
||||
|
||||
def _setupNodeForPainting(self, node: SceneNode) -> bool:
|
||||
mesh = node.getMeshData()
|
||||
if mesh.hasUVCoordinates():
|
||||
return True
|
||||
|
||||
texture_width, texture_height = mesh.calculateUnwrappedUVCoordinates()
|
||||
if texture_width <= 0 or texture_height <= 0:
|
||||
return False
|
||||
|
||||
node.callDecoration("prepareTexture", texture_width, texture_height)
|
||||
|
||||
if hasattr(mesh, OpenGL.VertexBufferProperty):
|
||||
# Force clear OpenGL buffer so that new UV coordinates will be sent
|
||||
delattr(mesh, OpenGL.VertexBufferProperty)
|
||||
|
||||
return True
|
||||
|
||||
def event(self, event: Event) -> bool:
|
||||
"""Handle mouse and keyboard events.
|
||||
|
||||
|
|
@ -397,16 +396,14 @@ class PaintTool(Tool):
|
|||
|
||||
# Make sure the displayed values are updated if the bounding box of the selected mesh(es) changes
|
||||
if event.type == Event.ToolActivateEvent:
|
||||
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.setActiveView("SolidView")
|
||||
CuraApplication.getInstance().getRenderer().getRenderPass("selection").setIgnoreUnselectedObjects(False)
|
||||
CuraApplication.getInstance().getRenderer().getRenderPass("selection_faces").setIgnoreUnselectedObjects(False)
|
||||
return True
|
||||
|
||||
if self._state != PaintTool.Paint.State.READY:
|
||||
return False
|
||||
|
||||
if event.type == Event.MouseReleaseEvent and self._controller.getToolsEnabled():
|
||||
if MouseEvent.LeftButton not in cast(MouseEvent, event).buttons:
|
||||
return False
|
||||
|
|
@ -459,9 +456,6 @@ class PaintTool(Tool):
|
|||
if not self._mesh_transformed_cache:
|
||||
return False
|
||||
|
||||
if not self._setupNodeForPainting(node):
|
||||
return False
|
||||
|
||||
face_id, _, world_coords = self._getCoordsFromClick(node, mouse_evt.x, mouse_evt.y)
|
||||
if face_id < 0:
|
||||
return False
|
||||
|
|
@ -484,6 +478,9 @@ class PaintTool(Tool):
|
|||
|
||||
return False
|
||||
|
||||
def getRequiredExtraRenderingPasses(self) -> list[str]:
|
||||
return ["selection_faces", "picking_selected"]
|
||||
|
||||
@staticmethod
|
||||
def _updateScene(node: SceneNode = None):
|
||||
if node is None:
|
||||
|
|
@ -491,11 +488,36 @@ class PaintTool(Tool):
|
|||
if node is not None:
|
||||
Application.getInstance().getController().getScene().sceneChanged.emit(node)
|
||||
|
||||
def getRequiredExtraRenderingPasses(self) -> list[str]:
|
||||
return ["selection_faces", "picking_selected"]
|
||||
def _onSelectionChanged(self):
|
||||
super()._onSelectionChanged()
|
||||
|
||||
self.setActiveView("PaintTool" if len(Selection.getAllSelectedObjects()) == 1 else None)
|
||||
self._updateState()
|
||||
|
||||
def _updateState(self):
|
||||
if len(Selection.getAllSelectedObjects()) == 1 and self._controller.getActiveTool() == self:
|
||||
selected_object = Selection.getSelectedObject(0)
|
||||
if selected_object.callDecoration("getPaintTexture") is not None:
|
||||
new_state = PaintTool.Paint.State.READY
|
||||
else:
|
||||
new_state = PaintTool.Paint.State.PREPARING_MODEL
|
||||
self._prepare_texture_job = PrepareTextureJob(selected_object)
|
||||
self._prepare_texture_job.finished.connect(self._onPrepareTextureFinished)
|
||||
self._prepare_texture_job.start()
|
||||
else:
|
||||
new_state = PaintTool.Paint.State.MULTIPLE_SELECTION
|
||||
|
||||
if new_state != self._state:
|
||||
self._state = new_state
|
||||
self.propertyChanged.emit()
|
||||
|
||||
def _onPrepareTextureFinished(self, job: Job):
|
||||
if job == self._prepare_texture_job:
|
||||
self._prepare_texture_job = None
|
||||
self._state = PaintTool.Paint.State.READY
|
||||
self.propertyChanged.emit()
|
||||
|
||||
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)
|
||||
ignore_unselected_objects = self._controller.getActiveView().name == "PaintTool"
|
||||
CuraApplication.getInstance().getRenderer().getRenderPass("selection").setIgnoreUnselectedObjects(ignore_unselected_objects)
|
||||
CuraApplication.getInstance().getRenderer().getRenderPass("selection_faces").setIgnoreUnselectedObjects(ignore_unselected_objects)
|
||||
|
|
@ -227,4 +227,75 @@ Item
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle
|
||||
{
|
||||
id: waitPrepareItem
|
||||
anchors.fill: parent
|
||||
color: UM.Theme.getColor("main_background")
|
||||
visible: UM.Controller.properties.getValue("State") === Cura.PaintToolState.PREPARING_MODEL
|
||||
|
||||
ColumnLayout
|
||||
{
|
||||
anchors.fill: parent
|
||||
|
||||
UM.Label
|
||||
{
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
Layout.verticalStretchFactor: 2
|
||||
|
||||
text: catalog.i18nc("@label", "Preparing model for painting...")
|
||||
verticalAlignment: Text.AlignBottom
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
|
||||
Item
|
||||
{
|
||||
Layout.preferredWidth: loadingIndicator.width
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.fillHeight: true
|
||||
Layout.verticalStretchFactor: 1
|
||||
|
||||
UM.ColorImage
|
||||
{
|
||||
id: loadingIndicator
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
width: UM.Theme.getSize("card_icon").width
|
||||
height: UM.Theme.getSize("card_icon").height
|
||||
source: UM.Theme.getIcon("ArrowDoubleCircleRight")
|
||||
color: UM.Theme.getColor("text_default")
|
||||
|
||||
RotationAnimator
|
||||
{
|
||||
target: loadingIndicator
|
||||
from: 0
|
||||
to: 360
|
||||
duration: 2000
|
||||
loops: Animation.Infinite
|
||||
running: true
|
||||
alwaysRunToEnd: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle
|
||||
{
|
||||
id: selectSingleMessageItem
|
||||
anchors.fill: parent
|
||||
color: UM.Theme.getColor("main_background")
|
||||
visible: UM.Controller.properties.getValue("State") === Cura.PaintToolState.MULTIPLE_SELECTION
|
||||
|
||||
UM.Label
|
||||
{
|
||||
anchors.fill: parent
|
||||
text: catalog.i18nc("@label", "Select a single model to start painting")
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,8 +9,8 @@ from PyQt6.QtGui import QImage, QColor, QPainter
|
|||
|
||||
from cura.CuraApplication import CuraApplication
|
||||
from cura.BuildVolume import BuildVolume
|
||||
from cura.CuraView import CuraView
|
||||
from UM.PluginRegistry import PluginRegistry
|
||||
from UM.View.View import View
|
||||
from UM.View.GL.ShaderProgram import ShaderProgram
|
||||
from UM.View.GL.Texture import Texture
|
||||
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
||||
|
|
@ -22,7 +22,7 @@ from UM.Math.Color import Color
|
|||
catalog = i18nCatalog("cura")
|
||||
|
||||
|
||||
class PaintView(View):
|
||||
class PaintView(CuraView):
|
||||
"""View for model-painting."""
|
||||
|
||||
UNDO_STACK_SIZE = 1024
|
||||
|
|
@ -33,7 +33,7 @@ class PaintView(View):
|
|||
self.value: int = value
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
super().__init__(use_empty_menu_placeholder = True)
|
||||
self._paint_shader: Optional[ShaderProgram] = None
|
||||
self._current_paint_texture: Optional[Texture] = None
|
||||
self._current_bits_ranges: tuple[int, int] = (0, 0)
|
||||
|
|
@ -50,8 +50,6 @@ class PaintView(View):
|
|||
application.engineCreatedSignal.connect(self._makePaintModes)
|
||||
self._scene = application.getController().getScene()
|
||||
|
||||
self._solid_view = None
|
||||
|
||||
def _makePaintModes(self):
|
||||
theme = CuraApplication.getInstance().getTheme()
|
||||
usual_types = {"none": self.PaintType(Color(*theme.getColor("paint_normal_area").getRgb()), 0),
|
||||
|
|
@ -179,18 +177,6 @@ class PaintView(View):
|
|||
if self._current_paint_type not in self._paint_modes:
|
||||
return
|
||||
|
||||
if self._solid_view is None:
|
||||
plugin_registry = PluginRegistry.getInstance()
|
||||
solid_view = plugin_registry.getPluginObject("SolidView")
|
||||
if isinstance(solid_view, View):
|
||||
self._solid_view = solid_view
|
||||
|
||||
display_objects = Selection.getAllSelectedObjects().copy()
|
||||
if len(display_objects) != 1 and self._solid_view is not None:
|
||||
# Display the classic view until a single object is selected
|
||||
self._solid_view.beginRendering()
|
||||
return
|
||||
|
||||
self._checkSetup()
|
||||
renderer = self.getRenderer()
|
||||
|
||||
|
|
@ -201,7 +187,7 @@ class PaintView(View):
|
|||
paint_batch = renderer.createRenderBatch(shader=self._paint_shader)
|
||||
renderer.addRenderBatch(paint_batch)
|
||||
|
||||
for node in display_objects:
|
||||
for node in Selection.getAllSelectedObjects():
|
||||
paint_batch.addItem(node.getWorldTransformation(copy=False), node.getMeshData(), normal_transformation=node.getCachedNormalMatrix())
|
||||
self._current_paint_texture = node.callDecoration("getPaintTexture")
|
||||
self._paint_shader.setTexture(0, self._current_paint_texture)
|
||||
|
|
|
|||
33
plugins/PaintTool/PrepareTextureJob.py
Normal file
33
plugins/PaintTool/PrepareTextureJob.py
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
# Copyright (c) 2025 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from UM.Job import Job
|
||||
from UM.Scene.SceneNode import SceneNode
|
||||
from UM.View.GL.OpenGL import OpenGL
|
||||
|
||||
|
||||
class PrepareTextureJob(Job):
|
||||
"""
|
||||
Background job to prepare a model for painting, i.e. do the UV-unwrapping and create the appropriate texture image,
|
||||
which can last a few seconds
|
||||
"""
|
||||
|
||||
def __init__(self, node: SceneNode):
|
||||
super().__init__()
|
||||
self._node: SceneNode = node
|
||||
|
||||
def run(self) -> None:
|
||||
# If the model has already-provided UV coordinates, we can only assume that the associated texture
|
||||
# should be a square
|
||||
texture_width = texture_height = 4096
|
||||
|
||||
mesh = self._node.getMeshData()
|
||||
if not mesh.hasUVCoordinates():
|
||||
texture_width, texture_height = mesh.calculateUnwrappedUVCoordinates()
|
||||
|
||||
self._node.callDecoration("prepareTexture", texture_width, texture_height)
|
||||
|
||||
if hasattr(mesh, OpenGL.VertexBufferProperty):
|
||||
# Force clear OpenGL buffer so that new UV coordinates will be sent
|
||||
delattr(mesh, OpenGL.VertexBufferProperty)
|
||||
|
||||
|
|
@ -27,6 +27,7 @@ def getMetaData():
|
|||
|
||||
def register(app):
|
||||
qmlRegisterUncreatableType(PaintTool.PaintTool.Brush, "Cura", 1, 0, "This is an enumeration class", "PaintToolBrush")
|
||||
qmlRegisterUncreatableType(PaintTool.PaintTool.Paint, "Cura", 1, 0, "This is an enumeration class", "PaintToolState")
|
||||
return {
|
||||
"tool": PaintTool.PaintTool(),
|
||||
"view": PaintView.PaintView()
|
||||
|
|
|
|||
|
|
@ -24,25 +24,6 @@ class PreviewStage(CuraStage):
|
|||
super().__init__(parent)
|
||||
self._application = application
|
||||
self._application.engineCreatedSignal.connect(self._engineCreated)
|
||||
self._previously_active_view = None # type: Optional[View]
|
||||
|
||||
def onStageSelected(self) -> None:
|
||||
"""When selecting the stage, remember which was the previous view so that
|
||||
|
||||
we can revert to that view when we go out of the stage later.
|
||||
"""
|
||||
self._previously_active_view = self._application.getController().getActiveView()
|
||||
|
||||
def onStageDeselected(self) -> None:
|
||||
"""Called when going to a different stage (away from the Preview Stage).
|
||||
|
||||
When going to a different stage, the view should be reverted to what it
|
||||
was before. Normally, that just reverts it to solid view.
|
||||
"""
|
||||
|
||||
if self._previously_active_view is not None:
|
||||
self._application.getController().setActiveView(self._previously_active_view.getPluginId())
|
||||
self._previously_active_view = None
|
||||
|
||||
def _engineCreated(self) -> None:
|
||||
"""Delayed load of the QML files.
|
||||
|
|
|
|||
|
|
@ -172,13 +172,20 @@ class SimulationView(CuraView):
|
|||
self._updateSliceWarningVisibility()
|
||||
self.activityChanged.emit()
|
||||
|
||||
def getSimulationPass(self) -> SimulationPass:
|
||||
def getSimulationPass(self) -> Optional[SimulationPass]:
|
||||
if not self._layer_pass:
|
||||
renderer = self.getRenderer()
|
||||
if renderer is None:
|
||||
return None
|
||||
|
||||
# Currently the RenderPass constructor requires a size > 0
|
||||
# This should be fixed in RenderPass's constructor.
|
||||
self._layer_pass = SimulationPass(1, 1)
|
||||
self._compatibility_mode = self._evaluateCompatibilityMode()
|
||||
self._layer_pass.setSimulationView(self)
|
||||
self._layer_pass.setEnabled(False)
|
||||
renderer.addRenderPass(self._layer_pass)
|
||||
|
||||
return self._layer_pass
|
||||
|
||||
def getCurrentLayer(self) -> int:
|
||||
|
|
@ -734,11 +741,14 @@ class SimulationView(CuraView):
|
|||
|
||||
# Make sure the SimulationPass is created
|
||||
layer_pass = self.getSimulationPass()
|
||||
if layer_pass is None:
|
||||
return False
|
||||
|
||||
renderer = self.getRenderer()
|
||||
if renderer is None:
|
||||
return False
|
||||
|
||||
renderer.addRenderPass(layer_pass)
|
||||
layer_pass.setEnabled(True)
|
||||
|
||||
# Make sure the NozzleNode is add to the root
|
||||
nozzle = self.getNozzleNode()
|
||||
|
|
@ -778,7 +788,7 @@ class SimulationView(CuraView):
|
|||
return False
|
||||
|
||||
if self._layer_pass is not None:
|
||||
renderer.removeRenderPass(self._layer_pass)
|
||||
self._layer_pass.setEnabled(False)
|
||||
if self._composite_pass:
|
||||
self._composite_pass.setLayerBindings(cast(List[str], self._old_layer_bindings))
|
||||
self._composite_pass.setCompositeShader(cast(ShaderProgram, self._old_composite_shader))
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ Cura.ExpandablePopup
|
|||
{
|
||||
if (activeView == null)
|
||||
{
|
||||
UM.Controller.setActiveView(viewModel.getItem(0).id)
|
||||
UM.Controller.activeStage.setActiveView(viewModel.getItem(0).id)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -110,7 +110,7 @@ Cura.ExpandablePopup
|
|||
onClicked:
|
||||
{
|
||||
toggleContent()
|
||||
UM.Controller.setActiveView(id)
|
||||
UM.Controller.activeStage.setActiveView(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue