mirror of
https://github.com/Ultimaker/Cura.git
synced 2026-01-08 23:57:52 -07:00
Optimize painting display performance
CURA-12731
This commit is contained in:
parent
3c6400823d
commit
6cf1f2df2a
3 changed files with 79 additions and 90 deletions
|
|
@ -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")
|
||||
|
||||
|
|
|
|||
|
|
@ -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())))
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue