From 968576472121c88735e0ebc5252336719738fac2 Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Mon, 4 Aug 2025 15:19:04 +0200 Subject: [PATCH 1/5] Implement undo-redo by full stroke CURA-12661 --- plugins/PaintTool/PaintTool.py | 4 +- plugins/PaintTool/PaintUndoCommand.py | 104 ++++++++++++++++++++++++++ plugins/PaintTool/PaintView.py | 95 ++++++++--------------- 3 files changed, 136 insertions(+), 67 deletions(-) create mode 100644 plugins/PaintTool/PaintUndoCommand.py diff --git a/plugins/PaintTool/PaintTool.py b/plugins/PaintTool/PaintTool.py index fa6436f10d..4f6516ffd7 100644 --- a/plugins/PaintTool/PaintTool.py +++ b/plugins/PaintTool/PaintTool.py @@ -130,7 +130,7 @@ class PaintTool(Tool): width, height = paintview.getUvTexDimensions() clear_image = QImage(width, height, QImage.Format.Format_RGB32) clear_image.fill(Qt.GlobalColor.white) - paintview.addStroke(clear_image, 0, 0, "none") + paintview.addStroke(clear_image, 0, 0, "none", False) self._updateScene() @@ -333,7 +333,7 @@ class PaintTool(Tool): end_coords[0] * w, end_coords[1] * h ) - paintview.addStroke(sub_image, start_x, start_y, self._brush_color) + paintview.addStroke(sub_image, start_x, start_y, self._brush_color, is_moved) self._last_text_coords = texcoords self._last_mouse_coords = (mouse_evt.x, mouse_evt.y) diff --git a/plugins/PaintTool/PaintUndoCommand.py b/plugins/PaintTool/PaintUndoCommand.py new file mode 100644 index 0000000000..c100780c7f --- /dev/null +++ b/plugins/PaintTool/PaintUndoCommand.py @@ -0,0 +1,104 @@ +# Copyright (c) 2025 UltiMaker +# Cura is released under the terms of the LGPLv3 or higher. + +from typing import cast, Optional + +from PyQt6.QtCore import QRect, QPoint +from PyQt6.QtGui import QUndoCommand, QImage, QPainter + +from UM.View.GL.Texture import Texture + + +class PaintUndoCommand(QUndoCommand): + """Provides the command that does the actual painting on objects with undo/redo mechanisms""" + + def __init__(self, + texture: Texture, + stroke_mask: QImage, + x: int, + y: int, + set_value: int, + bit_range: tuple[int, int], + mergeable: bool) -> None: + super().__init__() + + self._original_texture_image: Optional[QImage] = texture.getImage().copy() if not mergeable else None + self._texture: Texture = texture + self._stroke_mask: QImage = stroke_mask + self._x: int = x + self._y: int = y + self._set_value: int = set_value + self._bit_range: tuple[int, int] = bit_range + self._mergeable: bool = mergeable + + def id(self) -> int: + # Since the undo stack will contain only commands of this type, we can use a fixed ID + return 0 + + def redo(self) -> None: + actual_image = self._texture.getImage() + + bit_range_start, bit_range_end = self._bit_range + full_int32 = 0xffffffff + clear_mask = full_int32 ^ (((full_int32 << (32 - 1 - (bit_range_end - bit_range_start))) & full_int32) >> ( + 32 - 1 - bit_range_end)) + image_rect = QRect(0, 0, self._stroke_mask.width(), self._stroke_mask.height()) + + clear_bits_image = self._stroke_mask.copy() + clear_bits_image.invertPixels() + painter = QPainter(clear_bits_image) + painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_Lighten) + painter.fillRect(image_rect, clear_mask) + painter.end() + + set_value_image = self._stroke_mask.copy() + painter = QPainter(set_value_image) + painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_Multiply) + painter.fillRect(image_rect, self._set_value) + painter.end() + + stroked_image = actual_image.copy(self._x, self._y, self._stroke_mask.width(), self._stroke_mask.height()) + painter = QPainter(stroked_image) + painter.setCompositionMode(QPainter.CompositionMode.RasterOp_SourceAndDestination) + painter.drawImage(0, 0, clear_bits_image) + painter.setCompositionMode(QPainter.CompositionMode.RasterOp_SourceOrDestination) + painter.drawImage(0, 0, set_value_image) + painter.end() + + self._texture.setSubImage(stroked_image, self._x, self._y) + + def undo(self) -> None: + if self._original_texture_image is not None: + self._texture.setSubImage(self._original_texture_image.copy(self._x, + self._y, + self._stroke_mask.width(), + self._stroke_mask.height()), + self._x, + self._y) + + def mergeWith(self, command: QUndoCommand) -> bool: + if not isinstance(command, PaintUndoCommand): + return False + paint_undo_command = cast(PaintUndoCommand, command) + + if not paint_undo_command._mergeable: + return False + + self_rect = QRect(QPoint(self._x, self._y), self._stroke_mask.size()) + command_rect = QRect(QPoint(paint_undo_command._x, paint_undo_command._y), paint_undo_command._stroke_mask.size()) + bounding_rect = self_rect.united(command_rect) + + merged_mask = QImage(bounding_rect.width(), bounding_rect.height(), self._stroke_mask.format()) + merged_mask.fill(0) + + painter = QPainter(merged_mask) + painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_Lighten) + painter.drawImage(self._x - bounding_rect.x(), self._y - bounding_rect.y(), self._stroke_mask) + painter.drawImage(paint_undo_command._x - bounding_rect.x(), paint_undo_command._y - bounding_rect.y(), paint_undo_command._stroke_mask) + painter.end() + + self._x = bounding_rect.x() + self._y = bounding_rect.y() + self._stroke_mask = merged_mask + + return True diff --git a/plugins/PaintTool/PaintView.py b/plugins/PaintTool/PaintView.py index 749fa463e4..da00e16c2a 100644 --- a/plugins/PaintTool/PaintView.py +++ b/plugins/PaintTool/PaintView.py @@ -5,7 +5,7 @@ import os from PyQt6.QtCore import QRect from typing import Optional, List, Tuple, Dict -from PyQt6.QtGui import QImage, QColor, QPainter +from PyQt6.QtGui import QImage, QColor, QPainter, QUndoStack from cura.CuraApplication import CuraApplication from UM.PluginRegistry import PluginRegistry @@ -17,14 +17,14 @@ from UM.View.GL.OpenGL import OpenGL from UM.i18n import i18nCatalog from UM.Math.Color import Color +from .PaintUndoCommand import PaintUndoCommand + catalog = i18nCatalog("cura") class PaintView(View): """View for model-painting.""" - UNDO_STACK_SIZE = 1024 - class PaintType: def __init__(self, display_color: Color, value: int): self.display_color: Color = display_color @@ -38,8 +38,8 @@ class PaintView(View): self._current_paint_type = "" self._paint_modes: Dict[str, Dict[str, "PaintView.PaintType"]] = {} - self._stroke_undo_stack: List[Tuple[QImage, int, int]] = [] - self._stroke_redo_stack: List[Tuple[QImage, int, int]] = [] + self._paint_undo_stack: QUndoStack = QUndoStack() + self._paint_undo_stack.setUndoLimit(32) # Set a quite low amount since every command copies the full texture self._force_opaque_mask = QImage(2, 2, QImage.Format.Format_Mono) self._force_opaque_mask.fill(1) @@ -61,74 +61,39 @@ class PaintView(View): shader_filename = os.path.join(PluginRegistry.getInstance().getPluginPath("PaintTool"), "paint.shader") 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_mask: QImage, start_x: int, start_y: int, brush_color: str) -> None: + def addStroke(self, stroke_mask: QImage, start_x: int, start_y: int, brush_color: str, merge_with_previous: bool) -> None: if self._current_paint_texture is None or self._current_paint_texture.getImage() is None: return - actual_image = self._current_paint_texture.getImage() + current_image = self._current_paint_texture.getImage() + texture_rect = QRect(0, 0, current_image.width(), current_image.height()) + stroke_rect = QRect(start_x, start_y, stroke_mask.width(), stroke_mask.height()) + intersect_rect = texture_rect.intersected(stroke_rect) + if intersect_rect != stroke_rect: + # Stroke doesn't fully fit into the image, we have to crop it + stroke_mask = stroke_mask.copy(intersect_rect.x() - start_x, + intersect_rect.y() - start_y, + intersect_rect.width(), + intersect_rect.height()) + start_x = intersect_rect.x() + start_y = intersect_rect.y() bit_range_start, bit_range_end = self._current_bits_ranges - set_value = self._paint_modes[self._current_paint_type][brush_color].value << self._current_bits_ranges[0] - full_int32 = 0xffffffff - clear_mask = full_int32 ^ (((full_int32 << (32 - 1 - (bit_range_end - bit_range_start))) & full_int32) >> (32 - 1 - bit_range_end)) - image_rect = QRect(0, 0, stroke_mask.width(), stroke_mask.height()) + set_value = self._paint_modes[self._current_paint_type][brush_color].value << bit_range_start - clear_bits_image = stroke_mask.copy() - clear_bits_image.invertPixels() - painter = QPainter(clear_bits_image) - painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_Lighten) - painter.fillRect(image_rect, clear_mask) - painter.end() + self._paint_undo_stack.push(PaintUndoCommand(self._current_paint_texture, + stroke_mask, + start_x, + start_y, + set_value, + (bit_range_start, bit_range_end), + merge_with_previous)) - set_value_image = stroke_mask.copy() - painter = QPainter(set_value_image) - painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_Multiply) - painter.fillRect(image_rect, set_value) - painter.end() + def undoStroke(self) -> None: + self._paint_undo_stack.undo() - stroked_image = actual_image.copy(start_x, start_y, stroke_mask.width(), stroke_mask.height()) - painter = QPainter(stroked_image) - painter.setCompositionMode(QPainter.CompositionMode.RasterOp_SourceAndDestination) - painter.drawImage(0, 0, clear_bits_image) - painter.setCompositionMode(QPainter.CompositionMode.RasterOp_SourceOrDestination) - painter.drawImage(0, 0, set_value_image) - painter.end() - - 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(stroked_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 redoStroke(self) -> None: + self._paint_undo_stack.redo() def getUvTexDimensions(self): if self._current_paint_texture is not None: From 586c2939a60dd2750c44bdad383e5d66e3a82d0a Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Mon, 4 Aug 2025 15:46:51 +0200 Subject: [PATCH 2/5] Enable undo/redo buttons when appropriate CURA-12661 --- plugins/PaintTool/PaintTool.py | 68 ++++++++++++++------------------- plugins/PaintTool/PaintTool.qml | 20 +++++++++- plugins/PaintTool/PaintView.py | 20 +++++++--- plugins/PaintTool/__init__.py | 7 +++- 4 files changed, 65 insertions(+), 50 deletions(-) diff --git a/plugins/PaintTool/PaintTool.py b/plugins/PaintTool/PaintTool.py index b9f7a5e95b..293c069786 100644 --- a/plugins/PaintTool/PaintTool.py +++ b/plugins/PaintTool/PaintTool.py @@ -33,9 +33,13 @@ class PaintTool(Tool): SQUARE = 0 CIRCLE = 1 - def __init__(self) -> None: + def __init__(self, view: PaintView) -> None: super().__init__() + self._view: PaintView = view + self._view.canUndoChanged.connect(self._onCanUndoChanged) + self._view.canRedoChanged.connect(self._onCanRedoChanged) + self._picking_pass: Optional[PickingPass] = None self._faces_selection_pass: Optional[SelectionPass] = None @@ -56,7 +60,7 @@ 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.setExposedProperties("PaintType", "BrushSize", "BrushColor", "BrushShape", "CanUndo", "CanRedo") Selection.selectionChanged.connect(self._updateIgnoreUnselectedObjects) @@ -95,19 +99,11 @@ class PaintTool(Tool): return stroke_image, (start_x, start_y) def getPaintType(self) -> str: - paint_view = self._get_paint_view() - if paint_view is None: - return "" - - return paint_view.getPaintType() + return self._view.getPaintType() def setPaintType(self, paint_type: str) -> None: - paint_view = self._get_paint_view() - if paint_view is None: - return - if paint_type != self.getPaintType(): - paint_view.setPaintType(paint_type) + self._view.setPaintType(paint_type) self._brush_pen = self._createBrushPen() self._updateScene() @@ -140,38 +136,34 @@ class PaintTool(Tool): self._brush_pen = self._createBrushPen() self.propertyChanged.emit() - def undoStackAction(self, redo_instead: bool) -> bool: - paint_view = self._get_paint_view() - if paint_view is None: - return False + def getCanUndo(self) -> bool: + return self._view.canUndo() - if redo_instead: - paint_view.redoStroke() - else: - paint_view.undoStroke() + def _onCanUndoChanged(self): + self.propertyChanged.emit() + def getCanRedo(self) -> bool: + return self._view.canRedo() + + def _onCanRedoChanged(self): + self.propertyChanged.emit() + + def undoStackAction(self) -> None: + self._view.undoStroke() + self._updateScene() + + def redoStackAction(self) -> None: + self._view.redoStroke() self._updateScene() - return True def clear(self) -> None: - paintview = self._get_paint_view() - if paintview is None: - return - - width, height = paintview.getUvTexDimensions() + width, height = self._view.getUvTexDimensions() clear_image = QImage(width, height, QImage.Format.Format_RGB32) clear_image.fill(Qt.GlobalColor.white) - paintview.addStroke(clear_image, 0, 0, "none", False) + self._view.addStroke(clear_image, 0, 0, "none", False) self._updateScene() - @staticmethod - def _get_paint_view() -> Optional[PaintView]: - paint_view = Application.getInstance().getController().getActiveView() - if paint_view is None or paint_view.getPluginId() != "PaintTool": - return None - return cast(PaintView, paint_view) - @staticmethod 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 @@ -330,10 +322,6 @@ class PaintTool(Tool): else: self._mouse_held = True - paintview = self._get_paint_view() - if paintview is None: - return False - if not self._faces_selection_pass: self._faces_selection_pass = CuraApplication.getInstance().getRenderer().getRenderPass("selection_faces") if not self._faces_selection_pass: @@ -379,7 +367,7 @@ class PaintTool(Tool): (self._last_mouse_coords, (self._last_face_id, self._last_text_coords)), ((mouse_evt.x, mouse_evt.y), (face_id, texcoords))) - w, h = paintview.getUvTexDimensions() + w, h = self._view.getUvTexDimensions() for start_coords, end_coords in substrokes: sub_image, (start_x, start_y) = self._createStrokeImage( start_coords[0] * w, @@ -387,7 +375,7 @@ class PaintTool(Tool): end_coords[0] * w, end_coords[1] * h ) - paintview.addStroke(sub_image, start_x, start_y, self._brush_color, is_moved) + self._view.addStroke(sub_image, start_x, start_y, self._brush_color, is_moved) self._last_text_coords = texcoords self._last_mouse_coords = (mouse_evt.x, mouse_evt.y) diff --git a/plugins/PaintTool/PaintTool.qml b/plugins/PaintTool/PaintTool.qml index 2bb3106dd5..c448835bc5 100644 --- a/plugins/PaintTool/PaintTool.qml +++ b/plugins/PaintTool/PaintTool.qml @@ -19,14 +19,14 @@ Item { id: undoAction shortcut: "Ctrl+L" - onTriggered: UM.Controller.triggerActionWithData("undoStackAction", false) + onTriggered: UM.Controller.triggerAction("undoStackAction") } Action { id: redoAction shortcut: "Ctrl+Shift+L" - onTriggered: UM.Controller.triggerActionWithData("undoStackAction", true) + onTriggered: UM.Controller.triggerAction("redoStackAction") } Column @@ -192,6 +192,7 @@ Item { id: undoButton + enabled: undoAction.enabled text: catalog.i18nc("@action:button", "Undo Stroke") toolItem: UM.ColorImage { @@ -206,6 +207,7 @@ Item { id: redoButton + enabled: redoAction.enabled text: catalog.i18nc("@action:button", "Redo Stroke") toolItem: UM.ColorImage { @@ -227,4 +229,18 @@ Item } } } + + Binding + { + target: undoAction + property: "enabled" + value: UM.Controller.properties.getValue("CanUndo") + } + + Binding + { + target: redoAction + property: "enabled" + value: UM.Controller.properties.getValue("CanRedo") + } } diff --git a/plugins/PaintTool/PaintView.py b/plugins/PaintTool/PaintView.py index ed06da17d4..b5cd44772d 100644 --- a/plugins/PaintTool/PaintView.py +++ b/plugins/PaintTool/PaintView.py @@ -2,10 +2,10 @@ # Cura is released under the terms of the LGPLv3 or higher. import os -from PyQt6.QtCore import QRect -from typing import Optional, List, Tuple, Dict, cast +from PyQt6.QtCore import QRect, pyqtSignal +from typing import Optional, Dict -from PyQt6.QtGui import QImage, QColor, QPainter, QUndoStack +from PyQt6.QtGui import QImage, QUndoStack from cura.CuraApplication import CuraApplication from cura.BuildVolume import BuildVolume @@ -42,9 +42,8 @@ class PaintView(View): self._paint_undo_stack: QUndoStack = QUndoStack() self._paint_undo_stack.setUndoLimit(32) # Set a quite low amount since every command copies the full texture - - self._force_opaque_mask = QImage(2, 2, QImage.Format.Format_Mono) - self._force_opaque_mask.fill(1) + self._paint_undo_stack.canUndoChanged.connect(self.canUndoChanged) + self._paint_undo_stack.canRedoChanged.connect(self.canRedoChanged) application = CuraApplication.getInstance() application.engineCreatedSignal.connect(self._makePaintModes) @@ -52,6 +51,15 @@ class PaintView(View): self._solid_view = None + canUndoChanged = pyqtSignal(bool) + canRedoChanged = pyqtSignal(bool) + + def canUndo(self): + return self._paint_undo_stack.canUndo() + + def canRedo(self): + return self._paint_undo_stack.canRedo() + def _makePaintModes(self): theme = CuraApplication.getInstance().getTheme() usual_types = {"none": self.PaintType(Color(*theme.getColor("paint_normal_area").getRgb()), 0), diff --git a/plugins/PaintTool/__init__.py b/plugins/PaintTool/__init__.py index e92c169ee6..637e0b00f2 100644 --- a/plugins/PaintTool/__init__.py +++ b/plugins/PaintTool/__init__.py @@ -27,7 +27,10 @@ def getMetaData(): def register(app): qmlRegisterUncreatableType(PaintTool.PaintTool.Brush, "Cura", 1, 0, "This is an enumeration class", "PaintToolBrush") + + view = PaintView.PaintView() + return { - "tool": PaintTool.PaintTool(), - "view": PaintView.PaintView() + "tool": PaintTool.PaintTool(view), + "view": view } From 8f9a17d49e7ee40a28849ccdfc41671c198dbe27 Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Mon, 4 Aug 2025 16:03:09 +0200 Subject: [PATCH 3/5] Simplify QML code CURA-12661 --- plugins/PaintTool/BrushColorButton.qml | 25 ++----------------------- plugins/PaintTool/BrushShapeButton.qml | 25 ++----------------------- plugins/PaintTool/PaintModeButton.qml | 25 ++----------------------- plugins/PaintTool/PaintTool.qml | 24 ++++-------------------- 4 files changed, 10 insertions(+), 89 deletions(-) diff --git a/plugins/PaintTool/BrushColorButton.qml b/plugins/PaintTool/BrushColorButton.qml index ae4ab6243f..b62ab09e92 100644 --- a/plugins/PaintTool/BrushColorButton.qml +++ b/plugins/PaintTool/BrushColorButton.qml @@ -13,27 +13,6 @@ UM.ToolbarButton property string color - onClicked: setColor() - - function setColor() - { - UM.Controller.setProperty("BrushColor", buttonBrushColor.color); - } - - function isChecked() - { - return UM.Controller.properties.getValue("BrushColor") === buttonBrushColor.color; - } - - Component.onCompleted: - { - buttonBrushColor.checked = isChecked(); - } - - Binding - { - target: buttonBrushColor - property: "checked" - value: isChecked() - } + checked: UM.Controller.properties.getValue("BrushColor") === buttonBrushColor.color + onClicked: UM.Controller.setProperty("BrushColor", buttonBrushColor.color) } diff --git a/plugins/PaintTool/BrushShapeButton.qml b/plugins/PaintTool/BrushShapeButton.qml index ef4256792a..e05cd206f3 100644 --- a/plugins/PaintTool/BrushShapeButton.qml +++ b/plugins/PaintTool/BrushShapeButton.qml @@ -13,27 +13,6 @@ UM.ToolbarButton property int shape - onClicked: setShape() - - function setShape() - { - UM.Controller.setProperty("BrushShape", buttonBrushShape.shape) - } - - function isChecked() - { - return UM.Controller.properties.getValue("BrushShape") === buttonBrushShape.shape; - } - - Component.onCompleted: - { - buttonBrushShape.checked = isChecked(); - } - - Binding - { - target: buttonBrushShape - property: "checked" - value: isChecked() - } + checked: UM.Controller.properties.getValue("BrushShape") === buttonBrushShape.shape + onClicked: UM.Controller.setProperty("BrushShape", buttonBrushShape.shape) } diff --git a/plugins/PaintTool/PaintModeButton.qml b/plugins/PaintTool/PaintModeButton.qml index eb294f7ad6..833a009551 100644 --- a/plugins/PaintTool/PaintModeButton.qml +++ b/plugins/PaintTool/PaintModeButton.qml @@ -13,27 +13,6 @@ Cura.ModeSelectorButton property string mode - onClicked: setMode() - - function setMode() - { - UM.Controller.setProperty("PaintType", modeSelectorButton.mode); - } - - function isSelected() - { - return UM.Controller.properties.getValue("PaintType") === modeSelectorButton.mode; - } - - Component.onCompleted: - { - modeSelectorButton.selected = isSelected(); - } - - Binding - { - target: modeSelectorButton - property: "selected" - value: isSelected() - } + selected: UM.Controller.properties.getValue("PaintType") === modeSelectorButton.mode + onClicked: UM.Controller.setProperty("PaintType", modeSelectorButton.mode) } diff --git a/plugins/PaintTool/PaintTool.qml b/plugins/PaintTool/PaintTool.qml index c448835bc5..01d866967a 100644 --- a/plugins/PaintTool/PaintTool.qml +++ b/plugins/PaintTool/PaintTool.qml @@ -19,6 +19,7 @@ Item { id: undoAction shortcut: "Ctrl+L" + enabled: UM.Controller.properties.getValue("CanUndo") onTriggered: UM.Controller.triggerAction("undoStackAction") } @@ -26,6 +27,7 @@ Item { id: redoAction shortcut: "Ctrl+Shift+L" + enabled: UM.Controller.properties.getValue("CanRedo") onTriggered: UM.Controller.triggerAction("redoStackAction") } @@ -53,7 +55,7 @@ Item icon: "Support" tooltipText: catalog.i18nc("@tooltip", "Refine support placement by defining preferred/avoidance areas") mode: "support" - visible: false + // visible: false } } @@ -163,6 +165,7 @@ Item from: 10 to: 1000 + value: UM.Controller.properties.getValue("BrushSize") onPressedChanged: function(pressed) { @@ -171,11 +174,6 @@ Item UM.Controller.setProperty("BrushSize", shapeSizeSlider.value); } } - - Component.onCompleted: - { - shapeSizeSlider.value = UM.Controller.properties.getValue("BrushSize"); - } } //Line between the sections. @@ -229,18 +227,4 @@ Item } } } - - Binding - { - target: undoAction - property: "enabled" - value: UM.Controller.properties.getValue("CanUndo") - } - - Binding - { - target: redoAction - property: "enabled" - value: UM.Controller.properties.getValue("CanRedo") - } } From b5e2ce6168240406169fbb1b764ff5dce030fc81 Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Mon, 4 Aug 2025 16:07:49 +0200 Subject: [PATCH 4/5] Restore disabled support painting CURA-12661 --- plugins/PaintTool/PaintTool.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/PaintTool/PaintTool.qml b/plugins/PaintTool/PaintTool.qml index 01d866967a..0d1129eb60 100644 --- a/plugins/PaintTool/PaintTool.qml +++ b/plugins/PaintTool/PaintTool.qml @@ -55,7 +55,7 @@ Item icon: "Support" tooltipText: catalog.i18nc("@tooltip", "Refine support placement by defining preferred/avoidance areas") mode: "support" - // visible: false + visible: false } } From a55cca73f405d1d2b5029379425068532668b320 Mon Sep 17 00:00:00 2001 From: HellAholic Date: Tue, 26 Aug 2025 13:05:56 +0200 Subject: [PATCH 5/5] Apply Review clear_mask -> clear_texture_bit_mask --- plugins/PaintTool/PaintUndoCommand.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/PaintTool/PaintUndoCommand.py b/plugins/PaintTool/PaintUndoCommand.py index c100780c7f..50bfb787b7 100644 --- a/plugins/PaintTool/PaintUndoCommand.py +++ b/plugins/PaintTool/PaintUndoCommand.py @@ -40,7 +40,7 @@ class PaintUndoCommand(QUndoCommand): bit_range_start, bit_range_end = self._bit_range full_int32 = 0xffffffff - clear_mask = full_int32 ^ (((full_int32 << (32 - 1 - (bit_range_end - bit_range_start))) & full_int32) >> ( + clear_texture_bit_mask = full_int32 ^ (((full_int32 << (32 - 1 - (bit_range_end - bit_range_start))) & full_int32) >> ( 32 - 1 - bit_range_end)) image_rect = QRect(0, 0, self._stroke_mask.width(), self._stroke_mask.height()) @@ -48,7 +48,7 @@ class PaintUndoCommand(QUndoCommand): clear_bits_image.invertPixels() painter = QPainter(clear_bits_image) painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_Lighten) - painter.fillRect(image_rect, clear_mask) + painter.fillRect(image_rect, clear_texture_bit_mask) painter.end() set_value_image = self._stroke_mask.copy()