mirror of
https://github.com/Ultimaker/Cura.git
synced 2026-02-15 17:09:33 -07:00
Optimize painting operations
CURA-12743
This commit is contained in:
parent
05b3aeb2bd
commit
495a367539
5 changed files with 53 additions and 74 deletions
|
|
@ -20,10 +20,10 @@ class PaintClearCommand(PaintCommand):
|
|||
return 1
|
||||
|
||||
def redo(self) -> None:
|
||||
cleared_image, painter = self._makeClearedTexture()
|
||||
painter = self._makeClearedTexture()
|
||||
painter.end()
|
||||
|
||||
self._texture.setSubImage(cleared_image, 0, 0)
|
||||
self._texture.updateImagePart(self._bounding_rect)
|
||||
|
||||
def mergeWith(self, command: QUndoCommand) -> bool:
|
||||
if not isinstance(command, PaintClearCommand):
|
||||
|
|
|
|||
|
|
@ -23,9 +23,8 @@ class PaintCommand(QUndoCommand):
|
|||
self._bounding_rect = texture.getImage().rect()
|
||||
|
||||
if make_original_image:
|
||||
self._original_texture_image, painter = (
|
||||
self._preparePainting(specific_source_image=self._texture.getImage().copy(),
|
||||
specific_bounding_rect=self._texture.getImage().rect()))
|
||||
self._original_texture_image = self._texture.getImage().copy()
|
||||
painter = QPainter(self._original_texture_image)
|
||||
|
||||
# Keep only the bits contained in the bit range, so that we won't modify anything else in the image
|
||||
painter.setCompositionMode(QPainter.CompositionMode.RasterOp_SourceAndDestination)
|
||||
|
|
@ -36,19 +35,19 @@ class PaintCommand(QUndoCommand):
|
|||
if self._original_texture_image is None:
|
||||
return
|
||||
|
||||
cleared_image, painter = self._makeClearedTexture()
|
||||
|
||||
painter = self._makeClearedTexture()
|
||||
painter.setCompositionMode(QPainter.CompositionMode.RasterOp_SourceOrDestination)
|
||||
painter.drawImage(0, 0, self._original_texture_image)
|
||||
|
||||
painter.end()
|
||||
|
||||
self._texture.setSubImage(cleared_image, self._bounding_rect.left(), self._bounding_rect.top())
|
||||
self._texture.updateImagePart(self._bounding_rect)
|
||||
|
||||
def _makeClearedTexture(self) -> QPainter:
|
||||
painter = QPainter(self._texture.getImage())
|
||||
painter.setRenderHint(QPainter.RenderHint.Antialiasing, False)
|
||||
|
||||
def _makeClearedTexture(self) -> Tuple[QImage, QPainter]:
|
||||
dest_image, painter = self._preparePainting()
|
||||
self._clearTextureBits(painter)
|
||||
return dest_image, painter
|
||||
return painter
|
||||
|
||||
def _clearTextureBits(self, painter: QPainter):
|
||||
raise NotImplementedError()
|
||||
|
|
@ -57,16 +56,3 @@ class PaintCommand(QUndoCommand):
|
|||
bit_range_start, bit_range_end = self._bit_range
|
||||
return (((PaintCommand.FULL_INT32 << (32 - 1 - (bit_range_end - bit_range_start))) & PaintCommand.FULL_INT32) >>
|
||||
(32 - 1 - bit_range_end))
|
||||
|
||||
def _preparePainting(self,
|
||||
specific_source_image: Optional[QImage] = None,
|
||||
specific_bounding_rect: Optional[QRect] = None) -> Tuple[QImage, QPainter]:
|
||||
source_image = specific_source_image if specific_source_image is not None else self._texture.getImage()
|
||||
bounding_rect = specific_bounding_rect if specific_bounding_rect is not None else self._bounding_rect
|
||||
|
||||
dest_image = source_image.copy(bounding_rect)
|
||||
painter = QPainter(dest_image)
|
||||
painter.setRenderHint(QPainter.RenderHint.Antialiasing, False)
|
||||
painter.translate(-bounding_rect.left(), -bounding_rect.top())
|
||||
|
||||
return dest_image, painter
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
# Copyright (c) 2025 UltiMaker
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from typing import cast, Optional
|
||||
from typing import cast, Optional, List
|
||||
import math
|
||||
|
||||
from PyQt6.QtCore import QRect, QRectF, QPoint
|
||||
from PyQt6.QtGui import QUndoCommand, QImage, QPainter, QPainterPath, QPen, QBrush
|
||||
|
||||
from UM.View.GL.Texture import Texture
|
||||
from UM.Math.Polygon import Polygon
|
||||
|
||||
from .PaintCommand import PaintCommand
|
||||
|
||||
|
|
@ -18,13 +19,12 @@ class PaintStrokeCommand(PaintCommand):
|
|||
|
||||
def __init__(self,
|
||||
texture: Texture,
|
||||
stroke_path: QPainterPath,
|
||||
stroke_polygons: List[Polygon],
|
||||
set_value: int,
|
||||
bit_range: tuple[int, int],
|
||||
mergeable: bool) -> None:
|
||||
super().__init__(texture, bit_range, make_original_image = not mergeable)
|
||||
|
||||
self._stroke_path: QPainterPath = stroke_path
|
||||
self._stroke_polygons: List[Polygon] = stroke_polygons
|
||||
self._calculateBoundingRect()
|
||||
self._set_value: int = set_value
|
||||
self._mergeable: bool = mergeable
|
||||
|
|
@ -33,16 +33,14 @@ class PaintStrokeCommand(PaintCommand):
|
|||
return 0
|
||||
|
||||
def redo(self) -> None:
|
||||
stroked_image, painter = self._makeClearedTexture()
|
||||
|
||||
painter = self._makeClearedTexture()
|
||||
painter.setBrush(QBrush(self._set_value))
|
||||
painter.setPen(QPen(painter.brush(), self.PEN_OVERLAP_WIDTH))
|
||||
painter.setCompositionMode(QPainter.CompositionMode.RasterOp_SourceOrDestination)
|
||||
painter.drawPath(self._stroke_path)
|
||||
|
||||
painter.drawPath(self._makePainterPath())
|
||||
painter.end()
|
||||
|
||||
self._texture.setSubImage(stroked_image, self._bounding_rect.left(), self._bounding_rect.top())
|
||||
self._texture.updateImagePart(self._bounding_rect)
|
||||
|
||||
def mergeWith(self, command: QUndoCommand) -> bool:
|
||||
if not isinstance(command, PaintStrokeCommand):
|
||||
|
|
@ -52,7 +50,7 @@ class PaintStrokeCommand(PaintCommand):
|
|||
if not paint_undo_command._mergeable:
|
||||
return False
|
||||
|
||||
self._stroke_path = self._stroke_path.united(paint_undo_command._stroke_path)
|
||||
self._stroke_polygons = Polygon.union(self._stroke_polygons + paint_undo_command._stroke_polygons)
|
||||
self._calculateBoundingRect()
|
||||
|
||||
return True
|
||||
|
|
@ -61,10 +59,26 @@ class PaintStrokeCommand(PaintCommand):
|
|||
painter.setBrush(QBrush(self._getBitRangeMask()))
|
||||
painter.setPen(QPen(painter.brush(), self.PEN_OVERLAP_WIDTH))
|
||||
painter.setCompositionMode(QPainter.CompositionMode.RasterOp_NotSourceAndDestination)
|
||||
painter.drawPath(self._stroke_path)
|
||||
painter.drawPath(self._makePainterPath())
|
||||
|
||||
def _makePainterPath(self) -> QPainterPath:
|
||||
path = QPainterPath()
|
||||
for polygon in self._stroke_polygons:
|
||||
path.moveTo(polygon[0][0], polygon[0][1])
|
||||
for point in polygon:
|
||||
path.lineTo(point[0], point[1])
|
||||
path.closeSubpath()
|
||||
|
||||
return path
|
||||
|
||||
def _calculateBoundingRect(self):
|
||||
bounding_rect: QRectF = self._stroke_path.boundingRect()
|
||||
self._bounding_rect = QRect(
|
||||
QPoint(math.floor(bounding_rect.left()), math.floor(bounding_rect.top())),
|
||||
QPoint(math.ceil(bounding_rect.right()), math.ceil(bounding_rect.bottom())))
|
||||
bounding_box = Polygon.getGlobalBoundingBox(self._stroke_polygons)
|
||||
if bounding_box is None:
|
||||
self._bounding_rect = QRect()
|
||||
else:
|
||||
self._bounding_rect = QRect(
|
||||
QPoint(math.floor(bounding_box.left - PaintStrokeCommand.PEN_OVERLAP_WIDTH),
|
||||
math.floor(bounding_box.bottom - PaintStrokeCommand.PEN_OVERLAP_WIDTH)),
|
||||
QPoint(math.ceil(bounding_box.right + PaintStrokeCommand.PEN_OVERLAP_WIDTH),
|
||||
math.ceil(bounding_box.top + PaintStrokeCommand.PEN_OVERLAP_WIDTH)))
|
||||
self._bounding_rect &= self._texture.getImage().rect()
|
||||
|
|
@ -286,21 +286,8 @@ class PaintTool(Tool):
|
|||
def get_projected_on_plane(pt: numpy.ndarray) -> numpy.ndarray:
|
||||
return numpy.array([*self._camera.projectToViewport(Vector(*pt))], dtype=numpy.float32)
|
||||
|
||||
def get_projected_on_viewport_image(pt: numpy) -> numpy.ndarray:
|
||||
return numpy.array([pt[0] + self._camera.getViewportWidth() / 2.0,
|
||||
self._camera.getViewportHeight() - (pt[1] + self._camera.getViewportHeight() / 2.0)],
|
||||
dtype=numpy.float32)
|
||||
|
||||
stroke_poly = self._getStrokePolygon(get_projected_on_plane(world_coords_a), get_projected_on_plane(world_coords_b))
|
||||
stroke_poly.toType(numpy.float32)
|
||||
stroke_poly_viewport = Polygon([get_projected_on_viewport_image(point) for point in stroke_poly])
|
||||
|
||||
faces_image, (faces_x, faces_y) = PaintTool._rasterizePolygons([stroke_poly_viewport],
|
||||
QPen(Qt.PenStyle.NoPen),
|
||||
QBrush(Qt.GlobalColor.white))
|
||||
faces = self._faces_selection_pass.getFacesIdsUnderMask(faces_image, faces_x, faces_y)
|
||||
|
||||
texture_dimensions = numpy.array(list(self._view.getUvTexDimensions()))
|
||||
|
||||
mesh_indices = self._mesh_transformed_cache.getIndices()
|
||||
if mesh_indices is None:
|
||||
|
|
@ -319,8 +306,7 @@ class PaintTool(Tool):
|
|||
self._camera.getViewportHeight(),
|
||||
self._cam_norm,
|
||||
face_id)
|
||||
Logger.debug("done")
|
||||
return res
|
||||
return [Polygon(points) for points in res]
|
||||
|
||||
def event(self, event: Event) -> bool:
|
||||
"""Handle mouse and keyboard events.
|
||||
|
|
@ -412,12 +398,10 @@ class PaintTool(Tool):
|
|||
self._view.clearCursorStroke()
|
||||
|
||||
if self._mouse_held:
|
||||
Logger.debug("start stroking")
|
||||
uv_areas = self._getUvAreasForStroke(self._last_world_coords, world_coords, face_id)
|
||||
if len(uv_areas) == 0:
|
||||
return False
|
||||
stroke_path = self._createStrokePath(uv_areas)
|
||||
self._view.addStroke(stroke_path, brush_color, is_moved)
|
||||
self._view.addStroke(uv_areas, brush_color, is_moved)
|
||||
except:
|
||||
Logger.logException("e", "Error when adding paint stroke")
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ 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 UM.Logger import Logger
|
||||
from UM.Scene.SceneNode import SceneNode
|
||||
from cura.CuraApplication import CuraApplication
|
||||
from cura.BuildVolume import BuildVolume
|
||||
|
|
@ -21,6 +22,7 @@ from UM.Scene.Selection import Selection
|
|||
from UM.View.GL.OpenGL import OpenGL
|
||||
from UM.i18n import i18nCatalog
|
||||
from UM.Math.Color import Color
|
||||
from UM.Math.Polygon import Polygon
|
||||
|
||||
from .PaintStrokeCommand import PaintStrokeCommand
|
||||
from .PaintClearCommand import PaintClearCommand
|
||||
|
|
@ -162,23 +164,17 @@ class PaintView(CuraView):
|
|||
QPoint(math.floor(bounding_rect.left()), math.floor(bounding_rect.top())),
|
||||
QPoint(math.ceil(bounding_rect.right()), math.ceil(bounding_rect.bottom())))
|
||||
|
||||
cursor_image = QImage(bounding_rect_rounded.width(), bounding_rect_rounded.height(), QImage.Format.Format_ARGB32)
|
||||
cursor_image.fill(0)
|
||||
|
||||
painter = QPainter(cursor_image)
|
||||
painter = QPainter(self._cursor_texture.getImage())
|
||||
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.setBrush(QBrush(paint_color))
|
||||
painter.setPen(QPen(Qt.PenStyle.NoPen))
|
||||
painter.drawPath(cursor_path)
|
||||
|
||||
painter.end()
|
||||
|
||||
self._cursor_texture.setSubImage(cursor_image, bounding_rect_rounded.left(), bounding_rect_rounded.top())
|
||||
|
||||
self._cursor_texture.updateImagePart(bounding_rect_rounded)
|
||||
self._previous_paint_texture_rect = bounding_rect_rounded
|
||||
|
||||
def clearCursorStroke(self) -> bool:
|
||||
|
|
@ -186,18 +182,17 @@ class PaintView(CuraView):
|
|||
self._cursor_texture is None or self._cursor_texture.getImage() is None):
|
||||
return False
|
||||
|
||||
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_rect.left(),
|
||||
self._previous_paint_texture_rect.top())
|
||||
painter = QPainter(self._cursor_texture.getImage())
|
||||
painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_Source)
|
||||
painter.fillRect(self._previous_paint_texture_rect, QBrush(QColor(0, 0, 0, 0)))
|
||||
painter.end()
|
||||
|
||||
self._cursor_texture.updateImagePart(self._previous_paint_texture_rect)
|
||||
self._previous_paint_texture_rect = None
|
||||
|
||||
return True
|
||||
|
||||
def addStroke(self, stroke_path: QPainterPath, brush_color: str, merge_with_previous: bool) -> None:
|
||||
def addStroke(self, stroke_path: List[Polygon], brush_color: str, merge_with_previous: bool) -> None:
|
||||
if self._current_paint_texture is None or self._current_paint_texture.getImage() is None:
|
||||
return
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue