mirror of
https://git.mirrors.martin98.com/https://github.com/Ultimaker/Cura
synced 2025-08-15 01:05:58 +08:00
Merge remote-tracking branch 'origin/CURA-12543_painting_ux' into CURA-12544_saving-and-loading-painted-files-in-Cura
This commit is contained in:
commit
21443faa92
@ -41,9 +41,12 @@ class PaintTool(Tool):
|
|||||||
self._brush_size: int = 10
|
self._brush_size: int = 10
|
||||||
self._brush_color: str = "A"
|
self._brush_color: str = "A"
|
||||||
self._brush_shape: str = "A"
|
self._brush_shape: str = "A"
|
||||||
self._brush_pen: Optional[QPen] = None
|
self._brush_pen: QPen = self._createBrushPen()
|
||||||
|
|
||||||
self._mouse_held: bool = False
|
self._mouse_held: bool = False
|
||||||
|
self._ctrl_held: bool = False
|
||||||
|
self._shift_held: bool = False
|
||||||
|
|
||||||
self._last_text_coords: Optional[Tuple[int, int]] = None
|
self._last_text_coords: Optional[Tuple[int, int]] = None
|
||||||
|
|
||||||
def _createBrushPen(self) -> QPen:
|
def _createBrushPen(self) -> QPen:
|
||||||
@ -72,6 +75,9 @@ class PaintTool(Tool):
|
|||||||
painter = QPainter(stroke_image)
|
painter = QPainter(stroke_image)
|
||||||
painter.setRenderHint(QPainter.RenderHint.Antialiasing, False)
|
painter.setRenderHint(QPainter.RenderHint.Antialiasing, False)
|
||||||
painter.setPen(self._brush_pen)
|
painter.setPen(self._brush_pen)
|
||||||
|
if xdiff == 0 and ydiff == 0:
|
||||||
|
painter.drawPoint(int(x0 - start_x), int(y0 - start_y))
|
||||||
|
else:
|
||||||
painter.drawLine(int(x0 - start_x), int(y0 - start_y), int(x1 - start_x), int(y1 - start_y))
|
painter.drawLine(int(x0 - start_x), int(y0 - start_y), int(x1 - start_x), int(y1 - start_y))
|
||||||
painter.end()
|
painter.end()
|
||||||
|
|
||||||
@ -96,6 +102,20 @@ class PaintTool(Tool):
|
|||||||
self._brush_shape = brush_shape
|
self._brush_shape = brush_shape
|
||||||
self._brush_pen = self._createBrushPen()
|
self._brush_pen = self._createBrushPen()
|
||||||
|
|
||||||
|
def undoStackAction(self, redo_instead: bool) -> bool:
|
||||||
|
paintview = Application.getInstance().getController().getActiveView()
|
||||||
|
if paintview is None or paintview.getPluginId() != "PaintTool":
|
||||||
|
return False
|
||||||
|
paintview = cast(PaintView, paintview)
|
||||||
|
if redo_instead:
|
||||||
|
paintview.redoStroke()
|
||||||
|
else:
|
||||||
|
paintview.undoStroke()
|
||||||
|
nodes = Selection.getAllSelectedObjects()
|
||||||
|
if len(nodes) > 0:
|
||||||
|
Application.getInstance().getController().getScene().sceneChanged.emit(nodes[0])
|
||||||
|
return True
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _get_intersect_ratio_via_pt(a: numpy.ndarray, pt: numpy.ndarray, b: numpy.ndarray, c: numpy.ndarray) -> float:
|
def _get_intersect_ratio_via_pt(a: numpy.ndarray, pt: numpy.ndarray, b: numpy.ndarray, c: numpy.ndarray) -> float:
|
||||||
# compute the intersection of (param) A - pt with (param) B - (param) C
|
# compute the intersection of (param) A - pt with (param) B - (param) C
|
||||||
@ -154,6 +174,10 @@ class PaintTool(Tool):
|
|||||||
super().event(event)
|
super().event(event)
|
||||||
|
|
||||||
controller = Application.getInstance().getController()
|
controller = Application.getInstance().getController()
|
||||||
|
nodes = Selection.getAllSelectedObjects()
|
||||||
|
if len(nodes) <= 0:
|
||||||
|
return False
|
||||||
|
node = nodes[0]
|
||||||
|
|
||||||
# Make sure the displayed values are updated if the bounding box of the selected mesh(es) changes
|
# Make sure the displayed values are updated if the bounding box of the selected mesh(es) changes
|
||||||
if event.type == Event.ToolActivateEvent:
|
if event.type == Event.ToolActivateEvent:
|
||||||
@ -166,7 +190,27 @@ class PaintTool(Tool):
|
|||||||
controller.setActiveView("SolidView")
|
controller.setActiveView("SolidView")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
if event.type == Event.KeyPressEvent and cast(KeyEvent, event).key == KeyEvent.ShiftKey:
|
if event.type == Event.KeyPressEvent:
|
||||||
|
evt = cast(KeyEvent, event)
|
||||||
|
if evt.key == KeyEvent.ControlKey:
|
||||||
|
self._ctrl_held = True
|
||||||
|
return True
|
||||||
|
if evt.key == KeyEvent.ShiftKey:
|
||||||
|
self._shift_held = True
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
if event.type == Event.KeyReleaseEvent:
|
||||||
|
evt = cast(KeyEvent, event)
|
||||||
|
if evt.key == KeyEvent.ControlKey:
|
||||||
|
self._ctrl_held = False
|
||||||
|
return True
|
||||||
|
if evt.key == KeyEvent.ShiftKey:
|
||||||
|
self._shift_held = False
|
||||||
|
return True
|
||||||
|
if evt.key == Qt.Key.Key_L and self._ctrl_held:
|
||||||
|
# NOTE: Ctrl-L is used here instead of Ctrl-Z, as the latter is the application-wide one that takes precedence.
|
||||||
|
return self.undoStackAction(self._shift_held)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if event.type == Event.MouseReleaseEvent and self._controller.getToolsEnabled():
|
if event.type == Event.MouseReleaseEvent and self._controller.getToolsEnabled():
|
||||||
@ -201,10 +245,6 @@ class PaintTool(Tool):
|
|||||||
if not camera:
|
if not camera:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
node = Selection.getAllSelectedObjects()[0]
|
|
||||||
if node is None:
|
|
||||||
return False
|
|
||||||
|
|
||||||
if node != self._node_cache:
|
if node != self._node_cache:
|
||||||
if self._node_cache is not None:
|
if self._node_cache is not None:
|
||||||
self._node_cache.transformationChanged.disconnect(self._nodeTransformChanged)
|
self._node_cache.transformationChanged.disconnect(self._nodeTransformChanged)
|
||||||
|
@ -176,5 +176,40 @@ Item
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
RowLayout
|
||||||
|
{
|
||||||
|
UM.ToolbarButton
|
||||||
|
{
|
||||||
|
id: undoButton
|
||||||
|
|
||||||
|
text: catalog.i18nc("@action:button", "Undo Stroke")
|
||||||
|
toolItem: UM.ColorImage
|
||||||
|
{
|
||||||
|
source: UM.Theme.getIcon("ArrowReset")
|
||||||
|
}
|
||||||
|
property bool needBorder: true
|
||||||
|
|
||||||
|
z: 2
|
||||||
|
|
||||||
|
onClicked: UM.Controller.triggerActionWithData("undoStackAction", false)
|
||||||
|
}
|
||||||
|
|
||||||
|
UM.ToolbarButton
|
||||||
|
{
|
||||||
|
id: redoButton
|
||||||
|
|
||||||
|
text: catalog.i18nc("@action:button", "Redo Stroke")
|
||||||
|
toolItem: UM.ColorImage
|
||||||
|
{
|
||||||
|
source: UM.Theme.getIcon("ArrowDoubleCircleRight")
|
||||||
|
}
|
||||||
|
property bool needBorder: true
|
||||||
|
|
||||||
|
z: 2
|
||||||
|
|
||||||
|
onClicked: UM.Controller.triggerActionWithData("undoStackAction", true)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,9 +2,13 @@
|
|||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
import os
|
import os
|
||||||
from PyQt6.QtGui import QImage
|
from typing import Optional, List, Tuple
|
||||||
|
|
||||||
|
from PyQt6.QtGui import QImage, QColor, QPainter
|
||||||
|
|
||||||
from UM.PluginRegistry import PluginRegistry
|
from UM.PluginRegistry import PluginRegistry
|
||||||
|
from UM.View.GL.ShaderProgram import ShaderProgram
|
||||||
|
from UM.View.GL.Texture import Texture
|
||||||
from UM.View.View import View
|
from UM.View.View import View
|
||||||
from UM.Scene.Selection import Selection
|
from UM.Scene.Selection import Selection
|
||||||
from UM.View.GL.OpenGL import OpenGL
|
from UM.View.GL.OpenGL import OpenGL
|
||||||
@ -16,19 +20,63 @@ catalog = i18nCatalog("cura")
|
|||||||
class PaintView(View):
|
class PaintView(View):
|
||||||
"""View for model-painting."""
|
"""View for model-painting."""
|
||||||
|
|
||||||
|
UNDO_STACK_SIZE = 1024
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self._paint_shader = None
|
self._paint_shader: Optional[ShaderProgram] = None
|
||||||
self._current_paint_texture = None
|
self._current_paint_texture: Optional[Texture] = None
|
||||||
|
|
||||||
|
self._stroke_undo_stack: List[Tuple[QImage, int, int]] = []
|
||||||
|
self._stroke_redo_stack: List[Tuple[QImage, int, int]] = []
|
||||||
|
|
||||||
|
self._force_opaque_mask = QImage(2, 2, QImage.Format.Format_Mono)
|
||||||
|
self._force_opaque_mask.fill(1)
|
||||||
|
|
||||||
def _checkSetup(self):
|
def _checkSetup(self):
|
||||||
if not self._paint_shader:
|
if not self._paint_shader:
|
||||||
shader_filename = os.path.join(PluginRegistry.getInstance().getPluginPath("PaintTool"), "paint.shader")
|
shader_filename = os.path.join(PluginRegistry.getInstance().getPluginPath("PaintTool"), "paint.shader")
|
||||||
self._paint_shader = OpenGL.getInstance().createShaderProgram(shader_filename)
|
self._paint_shader = OpenGL.getInstance().createShaderProgram(shader_filename)
|
||||||
|
|
||||||
|
def _forceOpaqueDeepCopy(self, image: QImage):
|
||||||
|
res = QImage(image.width(), image.height(), QImage.Format.Format_RGBA8888)
|
||||||
|
res.fill(QColor(255, 255, 255, 255))
|
||||||
|
painter = QPainter(res)
|
||||||
|
painter.setRenderHint(QPainter.RenderHint.Antialiasing, False)
|
||||||
|
painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_SourceOver)
|
||||||
|
painter.drawImage(0, 0, image)
|
||||||
|
painter.end()
|
||||||
|
res.setAlphaChannel(self._force_opaque_mask.scaled(image.width(), image.height()))
|
||||||
|
return res
|
||||||
|
|
||||||
def addStroke(self, stroke_image: QImage, start_x: int, start_y: int) -> None:
|
def addStroke(self, stroke_image: QImage, start_x: int, start_y: int) -> None:
|
||||||
if self._current_paint_texture is not None:
|
if self._current_paint_texture is None:
|
||||||
self._current_paint_texture.setSubImage(stroke_image, start_x, start_y)
|
return
|
||||||
|
|
||||||
|
self._stroke_redo_stack.clear()
|
||||||
|
if len(self._stroke_undo_stack) >= PaintView.UNDO_STACK_SIZE:
|
||||||
|
self._stroke_undo_stack.pop(0)
|
||||||
|
undo_image = self._forceOpaqueDeepCopy(self._current_paint_texture.setSubImage(stroke_image, start_x, start_y))
|
||||||
|
if undo_image is not None:
|
||||||
|
self._stroke_undo_stack.append((undo_image, start_x, start_y))
|
||||||
|
|
||||||
|
def _applyUndoStacksAction(self, from_stack: List[Tuple[QImage, int, int]], to_stack: List[Tuple[QImage, int, int]]) -> bool:
|
||||||
|
if len(from_stack) <= 0 or self._current_paint_texture is None:
|
||||||
|
return False
|
||||||
|
from_image, x, y = from_stack.pop()
|
||||||
|
to_image = self._forceOpaqueDeepCopy(self._current_paint_texture.setSubImage(from_image, x, y))
|
||||||
|
if to_image is None:
|
||||||
|
return False
|
||||||
|
if len(to_stack) >= PaintView.UNDO_STACK_SIZE:
|
||||||
|
to_stack.pop(0)
|
||||||
|
to_stack.append((to_image, x, y))
|
||||||
|
return True
|
||||||
|
|
||||||
|
def undoStroke(self) -> bool:
|
||||||
|
return self._applyUndoStacksAction(self._stroke_undo_stack, self._stroke_redo_stack)
|
||||||
|
|
||||||
|
def redoStroke(self) -> bool:
|
||||||
|
return self._applyUndoStacksAction(self._stroke_redo_stack, self._stroke_undo_stack)
|
||||||
|
|
||||||
def getUvTexDimensions(self):
|
def getUvTexDimensions(self):
|
||||||
if self._current_paint_texture is not None:
|
if self._current_paint_texture is not None:
|
||||||
|
Loading…
x
Reference in New Issue
Block a user