Skip to content

Commit 21443fa

Browse files
committed
Merge remote-tracking branch 'origin/CURA-12543_painting_ux' into CURA-12544_saving-and-loading-painted-files-in-Cura
2 parents 5a4b5bf + d28c2aa commit 21443fa

File tree

3 files changed

+135
-12
lines changed

3 files changed

+135
-12
lines changed

plugins/PaintTool/PaintTool.py

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,12 @@ def __init__(self) -> None:
4141
self._brush_size: int = 10
4242
self._brush_color: str = "A"
4343
self._brush_shape: str = "A"
44-
self._brush_pen: Optional[QPen] = None
44+
self._brush_pen: QPen = self._createBrushPen()
4545

4646
self._mouse_held: bool = False
47+
self._ctrl_held: bool = False
48+
self._shift_held: bool = False
49+
4750
self._last_text_coords: Optional[Tuple[int, int]] = None
4851

4952
def _createBrushPen(self) -> QPen:
@@ -72,7 +75,10 @@ def _createStrokeImage(self, x0: float, y0: float, x1: float, y1: float) -> Tupl
7275
painter = QPainter(stroke_image)
7376
painter.setRenderHint(QPainter.RenderHint.Antialiasing, False)
7477
painter.setPen(self._brush_pen)
75-
painter.drawLine(int(x0 - start_x), int(y0 - start_y), int(x1 - start_x), int(y1 - start_y))
78+
if xdiff == 0 and ydiff == 0:
79+
painter.drawPoint(int(x0 - start_x), int(y0 - start_y))
80+
else:
81+
painter.drawLine(int(x0 - start_x), int(y0 - start_y), int(x1 - start_x), int(y1 - start_y))
7682
painter.end()
7783

7884
return stroke_image, (start_x, start_y)
@@ -96,6 +102,20 @@ def setBrushShape(self, brush_shape: str) -> None:
96102
self._brush_shape = brush_shape
97103
self._brush_pen = self._createBrushPen()
98104

105+
def undoStackAction(self, redo_instead: bool) -> bool:
106+
paintview = Application.getInstance().getController().getActiveView()
107+
if paintview is None or paintview.getPluginId() != "PaintTool":
108+
return False
109+
paintview = cast(PaintView, paintview)
110+
if redo_instead:
111+
paintview.redoStroke()
112+
else:
113+
paintview.undoStroke()
114+
nodes = Selection.getAllSelectedObjects()
115+
if len(nodes) > 0:
116+
Application.getInstance().getController().getScene().sceneChanged.emit(nodes[0])
117+
return True
118+
99119
@staticmethod
100120
def _get_intersect_ratio_via_pt(a: numpy.ndarray, pt: numpy.ndarray, b: numpy.ndarray, c: numpy.ndarray) -> float:
101121
# compute the intersection of (param) A - pt with (param) B - (param) C
@@ -154,6 +174,10 @@ def event(self, event: Event) -> bool:
154174
super().event(event)
155175

156176
controller = Application.getInstance().getController()
177+
nodes = Selection.getAllSelectedObjects()
178+
if len(nodes) <= 0:
179+
return False
180+
node = nodes[0]
157181

158182
# Make sure the displayed values are updated if the bounding box of the selected mesh(es) changes
159183
if event.type == Event.ToolActivateEvent:
@@ -166,7 +190,27 @@ def event(self, event: Event) -> bool:
166190
controller.setActiveView("SolidView")
167191
return True
168192

169-
if event.type == Event.KeyPressEvent and cast(KeyEvent, event).key == KeyEvent.ShiftKey:
193+
if event.type == Event.KeyPressEvent:
194+
evt = cast(KeyEvent, event)
195+
if evt.key == KeyEvent.ControlKey:
196+
self._ctrl_held = True
197+
return True
198+
if evt.key == KeyEvent.ShiftKey:
199+
self._shift_held = True
200+
return True
201+
return False
202+
203+
if event.type == Event.KeyReleaseEvent:
204+
evt = cast(KeyEvent, event)
205+
if evt.key == KeyEvent.ControlKey:
206+
self._ctrl_held = False
207+
return True
208+
if evt.key == KeyEvent.ShiftKey:
209+
self._shift_held = False
210+
return True
211+
if evt.key == Qt.Key.Key_L and self._ctrl_held:
212+
# NOTE: Ctrl-L is used here instead of Ctrl-Z, as the latter is the application-wide one that takes precedence.
213+
return self.undoStackAction(self._shift_held)
170214
return False
171215

172216
if event.type == Event.MouseReleaseEvent and self._controller.getToolsEnabled():
@@ -201,10 +245,6 @@ def event(self, event: Event) -> bool:
201245
if not camera:
202246
return False
203247

204-
node = Selection.getAllSelectedObjects()[0]
205-
if node is None:
206-
return False
207-
208248
if node != self._node_cache:
209249
if self._node_cache is not None:
210250
self._node_cache.transformationChanged.disconnect(self._nodeTransformChanged)

plugins/PaintTool/PaintTool.qml

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,5 +176,40 @@ Item
176176
}
177177
}
178178
}
179+
180+
RowLayout
181+
{
182+
UM.ToolbarButton
183+
{
184+
id: undoButton
185+
186+
text: catalog.i18nc("@action:button", "Undo Stroke")
187+
toolItem: UM.ColorImage
188+
{
189+
source: UM.Theme.getIcon("ArrowReset")
190+
}
191+
property bool needBorder: true
192+
193+
z: 2
194+
195+
onClicked: UM.Controller.triggerActionWithData("undoStackAction", false)
196+
}
197+
198+
UM.ToolbarButton
199+
{
200+
id: redoButton
201+
202+
text: catalog.i18nc("@action:button", "Redo Stroke")
203+
toolItem: UM.ColorImage
204+
{
205+
source: UM.Theme.getIcon("ArrowDoubleCircleRight")
206+
}
207+
property bool needBorder: true
208+
209+
z: 2
210+
211+
onClicked: UM.Controller.triggerActionWithData("undoStackAction", true)
212+
}
213+
}
179214
}
180215
}

plugins/PaintTool/PaintView.py

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,13 @@
22
# Cura is released under the terms of the LGPLv3 or higher.
33

44
import os
5-
from PyQt6.QtGui import QImage
5+
from typing import Optional, List, Tuple
6+
7+
from PyQt6.QtGui import QImage, QColor, QPainter
68

79
from UM.PluginRegistry import PluginRegistry
10+
from UM.View.GL.ShaderProgram import ShaderProgram
11+
from UM.View.GL.Texture import Texture
812
from UM.View.View import View
913
from UM.Scene.Selection import Selection
1014
from UM.View.GL.OpenGL import OpenGL
@@ -16,19 +20,63 @@
1620
class PaintView(View):
1721
"""View for model-painting."""
1822

23+
UNDO_STACK_SIZE = 1024
24+
1925
def __init__(self) -> None:
2026
super().__init__()
21-
self._paint_shader = None
22-
self._current_paint_texture = None
27+
self._paint_shader: Optional[ShaderProgram] = None
28+
self._current_paint_texture: Optional[Texture] = None
29+
30+
self._stroke_undo_stack: List[Tuple[QImage, int, int]] = []
31+
self._stroke_redo_stack: List[Tuple[QImage, int, int]] = []
32+
33+
self._force_opaque_mask = QImage(2, 2, QImage.Format.Format_Mono)
34+
self._force_opaque_mask.fill(1)
2335

2436
def _checkSetup(self):
2537
if not self._paint_shader:
2638
shader_filename = os.path.join(PluginRegistry.getInstance().getPluginPath("PaintTool"), "paint.shader")
2739
self._paint_shader = OpenGL.getInstance().createShaderProgram(shader_filename)
2840

41+
def _forceOpaqueDeepCopy(self, image: QImage):
42+
res = QImage(image.width(), image.height(), QImage.Format.Format_RGBA8888)
43+
res.fill(QColor(255, 255, 255, 255))
44+
painter = QPainter(res)
45+
painter.setRenderHint(QPainter.RenderHint.Antialiasing, False)
46+
painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_SourceOver)
47+
painter.drawImage(0, 0, image)
48+
painter.end()
49+
res.setAlphaChannel(self._force_opaque_mask.scaled(image.width(), image.height()))
50+
return res
51+
2952
def addStroke(self, stroke_image: QImage, start_x: int, start_y: int) -> None:
30-
if self._current_paint_texture is not None:
31-
self._current_paint_texture.setSubImage(stroke_image, start_x, start_y)
53+
if self._current_paint_texture is None:
54+
return
55+
56+
self._stroke_redo_stack.clear()
57+
if len(self._stroke_undo_stack) >= PaintView.UNDO_STACK_SIZE:
58+
self._stroke_undo_stack.pop(0)
59+
undo_image = self._forceOpaqueDeepCopy(self._current_paint_texture.setSubImage(stroke_image, start_x, start_y))
60+
if undo_image is not None:
61+
self._stroke_undo_stack.append((undo_image, start_x, start_y))
62+
63+
def _applyUndoStacksAction(self, from_stack: List[Tuple[QImage, int, int]], to_stack: List[Tuple[QImage, int, int]]) -> bool:
64+
if len(from_stack) <= 0 or self._current_paint_texture is None:
65+
return False
66+
from_image, x, y = from_stack.pop()
67+
to_image = self._forceOpaqueDeepCopy(self._current_paint_texture.setSubImage(from_image, x, y))
68+
if to_image is None:
69+
return False
70+
if len(to_stack) >= PaintView.UNDO_STACK_SIZE:
71+
to_stack.pop(0)
72+
to_stack.append((to_image, x, y))
73+
return True
74+
75+
def undoStroke(self) -> bool:
76+
return self._applyUndoStacksAction(self._stroke_undo_stack, self._stroke_redo_stack)
77+
78+
def redoStroke(self) -> bool:
79+
return self._applyUndoStacksAction(self._stroke_redo_stack, self._stroke_undo_stack)
3280

3381
def getUvTexDimensions(self):
3482
if self._current_paint_texture is not None:

0 commit comments

Comments
 (0)