Optimize painting display performance

CURA-12731
This commit is contained in:
Erwan MATHIEU 2025-09-19 16:01:41 +02:00
parent 3c6400823d
commit 6cf1f2df2a
3 changed files with 79 additions and 90 deletions

View file

@ -5,7 +5,7 @@ import math
from enum import IntEnum
import numpy
from PyQt6.QtCore import Qt, QObject, pyqtEnum, QPointF
from PyQt6.QtGui import QImage, QPainter, QPen, QBrush, QPolygonF
from PyQt6.QtGui import QImage, QPainter, QPen, QBrush, QPolygonF, QPainterPath
from typing import cast, Optional, Tuple, List
from UM.Application import Application
@ -111,8 +111,16 @@ class PaintTool(Tool):
Logger.error(f"Unknown brush shape '{self._brush_shape}', painting may not work.")
return pen
def _createStrokeImage(self, polys: List[Polygon]) -> Tuple[QImage, Tuple[int, int]]:
return PaintTool._rasterizePolygons(polys, self._brush_pen, QBrush(self._brush_pen.color()))
def _createStrokePath(self, polygons: List[Polygon]) -> QPainterPath:
path = QPainterPath()
for polygon in polygons:
path.moveTo(polygon[0][0], polygon[0][1])
for point in polygon:
path.lineTo(point[0], point[1])
path.closeSubpath()
return path
def getPaintType(self) -> str:
return self._view.getPaintType()
@ -434,8 +442,8 @@ class PaintTool(Tool):
brush_color = self._brush_color if self.getPaintType() != "extruder" else str(self._brush_extruder)
uv_areas_cursor = self._getUvAreasForStroke(world_coords, world_coords)
if len(uv_areas_cursor) > 0:
cursor_stroke_img, (start_x, start_y) = self._createStrokeImage(uv_areas_cursor)
self._view.setCursorStroke(cursor_stroke_img, start_x, start_y, brush_color)
cursor_path = self._createStrokePath(uv_areas_cursor)
self._view.setCursorStroke(cursor_path, brush_color)
else:
self._view.clearCursorStroke()
@ -443,8 +451,8 @@ class PaintTool(Tool):
uv_areas = self._getUvAreasForStroke(self._last_world_coords, world_coords)
if len(uv_areas) == 0:
return False
stroke_img, (start_x, start_y) = self._createStrokeImage(uv_areas)
self._view.addStroke(stroke_img, start_x, start_y, brush_color, is_moved)
stroke_path = self._createStrokePath(uv_areas)
self._view.addStroke(stroke_path, brush_color, is_moved)
except:
Logger.logException("e", "Error when adding paint stroke")

View file

@ -2,21 +2,23 @@
# Cura is released under the terms of the LGPLv3 or higher.
from typing import cast, Optional
import math
from PyQt6.QtCore import QRect, QPoint
from PyQt6.QtGui import QUndoCommand, QImage, QPainter
from PyQt6.QtCore import Qt, QRect, QPoint
from PyQt6.QtGui import QUndoCommand, QImage, QPainter, QPainterPath, QPen, QBrush
from UM.View.GL.Texture import Texture
from UM.Logger import Logger
class PaintUndoCommand(QUndoCommand):
"""Provides the command that does the actual painting on objects with undo/redo mechanisms"""
PEN_OVERLAP_WIDTH = 2.5
def __init__(self,
texture: Texture,
stroke_mask: QImage,
x: int,
y: int,
stroke_path: QPainterPath,
set_value: int,
bit_range: tuple[int, int],
mergeable: bool) -> None:
@ -24,9 +26,8 @@ class PaintUndoCommand(QUndoCommand):
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._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
@ -38,43 +39,39 @@ class PaintUndoCommand(QUndoCommand):
def redo(self) -> None:
actual_image = self._texture.getImage()
bounding_rect = self._stroke_path.boundingRect()
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))
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_texture_bit_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())
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.setPen(QPen(painter.brush(), self.PEN_OVERLAP_WIDTH))
painter.setCompositionMode(QPainter.CompositionMode.RasterOp_SourceAndDestination)
painter.drawImage(0, 0, clear_bits_image)
painter.drawPath(self._stroke_path)
painter.setBrush(QBrush(self._set_value))
painter.setPen(QPen(painter.brush(), self.PEN_OVERLAP_WIDTH))
painter.setCompositionMode(QPainter.CompositionMode.RasterOp_SourceOrDestination)
painter.drawImage(0, 0, set_value_image)
painter.drawPath(self._stroke_path)
painter.end()
self._texture.setSubImage(stroked_image, self._x, self._y)
self._texture.setSubImage(stroked_image, bounding_rect_rounded.left(), bounding_rect_rounded.top())
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)
self._texture.setSubImage(self._original_texture_image.copy(self._bounding_rect_rounded),
self._bounding_rect_rounded.left(),
self._bounding_rect_rounded.top())
def mergeWith(self, command: QUndoCommand) -> bool:
if not isinstance(command, PaintUndoCommand):
@ -84,21 +81,13 @@ class PaintUndoCommand(QUndoCommand):
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
self._stroke_path = self._stroke_path.united(paint_undo_command._stroke_path)
self._calculateBoundingRect()
return True
def _calculateBoundingRect(self):
self._bounding_rect = self._stroke_path.boundingRect()
self._bounding_rect_rounded = QRect(
QPoint(math.floor(self._bounding_rect.left()), math.floor(self._bounding_rect.top())),
QPoint(math.ceil(self._bounding_rect.right()), math.ceil(self._bounding_rect.bottom())))

View file

@ -2,9 +2,10 @@
# Cura is released under the terms of the LGPLv3 or higher.
import os
import math
from PyQt6.QtCore import QRect, pyqtSignal
from PyQt6.QtGui import QImage, QUndoStack, QPainter, QColor
from PyQt6.QtCore import QRect, pyqtSignal, Qt, QPoint
from PyQt6.QtGui import QImage, QUndoStack, QPainter, QColor, QPainterPath, QBrush, QPen
from typing import Optional, List, Tuple, Dict
from cura.CuraApplication import CuraApplication
@ -37,7 +38,7 @@ class PaintView(CuraView):
super().__init__(use_empty_menu_placeholder = True)
self._paint_shader: Optional[ShaderProgram] = None
self._current_paint_texture: Optional[Texture] = None
self._previous_paint_texture_stroke: Optional[QRect] = None
self._previous_paint_texture_rect: Optional[QRect] = None
self._cursor_texture: Optional[Texture] = None
self._current_bits_ranges: tuple[int, int] = (0, 0)
self._current_paint_type = ""
@ -116,72 +117,63 @@ class PaintView(CuraView):
shader_filename = os.path.join(PluginRegistry.getInstance().getPluginPath("PaintTool"), "paint.shader")
self._paint_shader = OpenGL.getInstance().createShaderProgram(shader_filename)
def setCursorStroke(self, stroke_mask: QImage, start_x: int, start_y: int, brush_color: str):
def setCursorStroke(self, cursor_path: QPainterPath, brush_color: str):
if self._cursor_texture is None or self._cursor_texture.getImage() is None:
return
self.clearCursorStroke()
stroke_image = stroke_mask.copy()
alpha_mask = stroke_image.convertedTo(QImage.Format.Format_Mono)
stroke_image.setAlphaChannel(alpha_mask)
bounding_rect = cursor_path.boundingRect()
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())))
painter = QPainter(stroke_image)
cursor_image = QImage(bounding_rect_rounded.width(), bounding_rect_rounded.height(), QImage.Format.Format_ARGB32)
cursor_image.fill(0)
painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_SourceAtop)
painter = QPainter(cursor_image)
painter.setRenderHint(QPainter.RenderHint.Antialiasing, False)
painter.translate(-bounding_rect.left(), -bounding_rect.top())
display_color = self._paint_modes[self._current_paint_type][brush_color].display_color
paint_color = QColor(*[int(color_part * 255) for color_part in [display_color.r, display_color.g, display_color.b]])
paint_color.setAlpha(255)
painter.fillRect(0, 0, stroke_mask.width(), stroke_mask.height(), paint_color)
painter.setBrush(QBrush(paint_color))
painter.setPen(QPen(Qt.PenStyle.NoPen))
painter.drawPath(cursor_path)
painter.end()
self._cursor_texture.setSubImage(stroke_image, start_x, start_y)
self._cursor_texture.setSubImage(cursor_image, bounding_rect_rounded.left(), bounding_rect_rounded.top())
self._previous_paint_texture_stroke = QRect(start_x, start_y, stroke_mask.width(), stroke_mask.height())
self._previous_paint_texture_rect = bounding_rect_rounded
def clearCursorStroke(self) -> bool:
if (self._previous_paint_texture_stroke is None or
if (self._previous_paint_texture_rect is None or
self._cursor_texture is None or self._cursor_texture.getImage() is None):
return False
clear_image = QImage(self._previous_paint_texture_stroke.width(),
self._previous_paint_texture_stroke.height(),
clear_image = QImage(self._previous_paint_texture_rect.width(),
self._previous_paint_texture_rect.height(),
QImage.Format.Format_ARGB32)
clear_image.fill(0)
self._cursor_texture.setSubImage(clear_image,
self._previous_paint_texture_stroke.x(),
self._previous_paint_texture_stroke.y())
self._previous_paint_texture_stroke = None
self._previous_paint_texture_rect.left(),
self._previous_paint_texture_rect.top())
self._previous_paint_texture_rect = None
return True
def addStroke(self, stroke_mask: QImage, start_x: int, start_y: int, brush_color: str, merge_with_previous: bool) -> None:
def addStroke(self, stroke_path: QPainterPath, brush_color: str, merge_with_previous: bool) -> None:
if self._current_paint_texture is None or self._current_paint_texture.getImage() is None:
return
self._prepareDataMapping()
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 << bit_range_start
self._paint_undo_stack.push(PaintUndoCommand(self._current_paint_texture,
stroke_mask,
start_x,
start_y,
stroke_path,
set_value,
(bit_range_start, bit_range_end),
merge_with_previous))