Fix clear painting with optimized painting mechanisms

CURA-12731
This commit is contained in:
Erwan MATHIEU 2025-09-22 11:40:44 +02:00
parent c890a553dd
commit a2db7b3004
5 changed files with 80 additions and 18 deletions

View file

@ -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

View file

@ -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))

View file

@ -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

View file

@ -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)

View file

@ -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()