From a2db7b3004f99a3a3f207cffe8bee3c0c73e6b8e Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Mon, 22 Sep 2025 11:40:44 +0200 Subject: [PATCH] Fix clear painting with optimized painting mechanisms CURA-12731 --- plugins/PaintTool/PaintClearCommand.py | 44 ++++++++++++++++++++++++++ plugins/PaintTool/PaintCommand.py | 22 +++++++++++++ plugins/PaintTool/PaintTool.py | 6 +--- plugins/PaintTool/PaintUndoCommand.py | 18 +++-------- plugins/PaintTool/PaintView.py | 8 +++++ 5 files changed, 80 insertions(+), 18 deletions(-) create mode 100644 plugins/PaintTool/PaintClearCommand.py create mode 100644 plugins/PaintTool/PaintCommand.py diff --git a/plugins/PaintTool/PaintClearCommand.py b/plugins/PaintTool/PaintClearCommand.py new file mode 100644 index 0000000000..ff69930027 --- /dev/null +++ b/plugins/PaintTool/PaintClearCommand.py @@ -0,0 +1,44 @@ +# Copyright (c) 2025 UltiMaker +# Cura is released under the terms of the LGPLv3 or higher. + +from typing import Optional + +from PyQt6.QtGui import QUndoCommand, QImage, QPainter, QBrush + +from UM.View.GL.Texture import Texture + +from .PaintCommand import PaintCommand + + +class PaintClearCommand(PaintCommand): + """Provides the command that clear all the painting for the current mode""" + + def __init__(self, texture: Texture, bit_range: tuple[int, int]) -> None: + super().__init__(texture, bit_range) + + self._original_texture_image: Optional[QImage] = texture.getImage().copy() + + self._cleared_texture = self._original_texture_image.copy() + painter = QPainter(self._cleared_texture) + + painter.setRenderHint(QPainter.RenderHint.Antialiasing, False) + painter.setCompositionMode(QPainter.CompositionMode.RasterOp_SourceAndDestination) + painter.fillRect(self._cleared_texture.rect(), QBrush(self._getClearTextureBitMask())) + + painter.end() + + def id(self) -> int: + return 1 + + def redo(self) -> None: + self._texture.setSubImage(self._cleared_texture, 0, 0) + + def undo(self) -> None: + self._texture.setSubImage(self._original_texture_image, 0, 0) + + def mergeWith(self, command: QUndoCommand) -> bool: + if not isinstance(command, PaintClearCommand): + return False + + # There is actually nothing more to do here, both clear commands already have the same original texture + return True diff --git a/plugins/PaintTool/PaintCommand.py b/plugins/PaintTool/PaintCommand.py new file mode 100644 index 0000000000..8c78a13697 --- /dev/null +++ b/plugins/PaintTool/PaintCommand.py @@ -0,0 +1,22 @@ +# Copyright (c) 2025 UltiMaker +# Cura is released under the terms of the LGPLv3 or higher. + +from PyQt6.QtGui import QUndoCommand + +from UM.View.GL.Texture import Texture + + +class PaintCommand(QUndoCommand): + """Provides a command that somehow modifies the actual painting on objects with undo/redo mechanisms""" + + def __init__(self, texture: Texture, bit_range: tuple[int, int]) -> None: + super().__init__() + + self._texture: Texture = texture + self._bit_range: tuple[int, int] = bit_range + + def _getClearTextureBitMask(self): + bit_range_start, bit_range_end = self._bit_range + full_int32 = 0xffffffff + return full_int32 ^ (((full_int32 << (32 - 1 - (bit_range_end - bit_range_start))) & full_int32) >> + (32 - 1 - bit_range_end)) diff --git a/plugins/PaintTool/PaintTool.py b/plugins/PaintTool/PaintTool.py index 3275b2ef64..582fa35bd1 100644 --- a/plugins/PaintTool/PaintTool.py +++ b/plugins/PaintTool/PaintTool.py @@ -192,11 +192,7 @@ class PaintTool(Tool): self._updateScene() def clear(self) -> None: - width, height = self._view.getUvTexDimensions() - clear_image = QImage(width, height, QImage.Format.Format_RGB32) - clear_image.fill(Qt.GlobalColor.white) - self._view.addStroke(clear_image, 0, 0, "none" if self.getPaintType() != "extruder" else "0", False) - + self._view.clearPaint() self._updateScene() @staticmethod diff --git a/plugins/PaintTool/PaintUndoCommand.py b/plugins/PaintTool/PaintUndoCommand.py index 19e69eb147..321403a6de 100644 --- a/plugins/PaintTool/PaintUndoCommand.py +++ b/plugins/PaintTool/PaintUndoCommand.py @@ -4,14 +4,14 @@ from typing import cast, Optional import math -from PyQt6.QtCore import Qt, QRect, QPoint +from PyQt6.QtCore import QRect, QPoint from PyQt6.QtGui import QUndoCommand, QImage, QPainter, QPainterPath, QPen, QBrush from UM.View.GL.Texture import Texture -from UM.Logger import Logger +from .PaintCommand import PaintCommand -class PaintUndoCommand(QUndoCommand): +class PaintUndoCommand(PaintCommand): """Provides the command that does the actual painting on objects with undo/redo mechanisms""" PEN_OVERLAP_WIDTH = 2.5 @@ -22,18 +22,15 @@ class PaintUndoCommand(QUndoCommand): set_value: int, bit_range: tuple[int, int], mergeable: bool) -> None: - super().__init__() + super().__init__(texture, bit_range) self._original_texture_image: Optional[QImage] = texture.getImage().copy() if not mergeable else None - self._texture: Texture = texture self._stroke_path: QPainterPath = stroke_path self._calculateBoundingRect() 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: @@ -43,17 +40,12 @@ class PaintUndoCommand(QUndoCommand): bounding_rect_rounded = QRect(QPoint(math.floor(bounding_rect.left()), math.floor(bounding_rect.top())), QPoint(math.ceil(bounding_rect.right()), math.ceil(bounding_rect.bottom()))) - bit_range_start, bit_range_end = self._bit_range - full_int32 = 0xffffffff - clear_texture_bit_mask = full_int32 ^ (((full_int32 << (32 - 1 - (bit_range_end - bit_range_start))) & full_int32) >> ( - 32 - 1 - bit_range_end)) - stroked_image = actual_image.copy(bounding_rect_rounded) painter = QPainter(stroked_image) painter.setRenderHint(QPainter.RenderHint.Antialiasing, False) painter.translate(-bounding_rect.left(), -bounding_rect.top()) - painter.setBrush(QBrush(clear_texture_bit_mask)) + painter.setBrush(QBrush(self._getClearTextureBitMask())) painter.setPen(QPen(painter.brush(), self.PEN_OVERLAP_WIDTH)) painter.setCompositionMode(QPainter.CompositionMode.RasterOp_SourceAndDestination) painter.drawPath(self._stroke_path) diff --git a/plugins/PaintTool/PaintView.py b/plugins/PaintTool/PaintView.py index c2eb81935c..78c1fd8bd4 100644 --- a/plugins/PaintTool/PaintView.py +++ b/plugins/PaintTool/PaintView.py @@ -22,6 +22,7 @@ from UM.i18n import i18nCatalog from UM.Math.Color import Color from .PaintUndoCommand import PaintUndoCommand +from .PaintClearCommand import PaintClearCommand catalog = i18nCatalog("cura") @@ -178,6 +179,13 @@ class PaintView(CuraView): (bit_range_start, bit_range_end), merge_with_previous)) + def clearPaint(self): + if self._current_paint_texture is None or self._current_paint_texture.getImage() is None: + return + + self._prepareDataMapping() + self._paint_undo_stack.push(PaintClearCommand(self._current_paint_texture, self._current_bits_ranges)) + def undoStroke(self) -> None: self._paint_undo_stack.undo()