diff --git a/plugins/PaintTool/PaintClearCommand.py b/plugins/PaintTool/PaintClearCommand.py index 7ee8993bd3..1e087c6b6f 100644 --- a/plugins/PaintTool/PaintClearCommand.py +++ b/plugins/PaintTool/PaintClearCommand.py @@ -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): diff --git a/plugins/PaintTool/PaintCommand.py b/plugins/PaintTool/PaintCommand.py index 38f1dbc6e4..65542b1cc2 100644 --- a/plugins/PaintTool/PaintCommand.py +++ b/plugins/PaintTool/PaintCommand.py @@ -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 diff --git a/plugins/PaintTool/PaintStrokeCommand.py b/plugins/PaintTool/PaintStrokeCommand.py index 969d5d8a81..8d4a5c2dbd 100644 --- a/plugins/PaintTool/PaintStrokeCommand.py +++ b/plugins/PaintTool/PaintStrokeCommand.py @@ -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()))) \ No newline at end of file + 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() \ No newline at end of file diff --git a/plugins/PaintTool/PaintTool.py b/plugins/PaintTool/PaintTool.py index f1d831ac7b..5850c967fd 100644 --- a/plugins/PaintTool/PaintTool.py +++ b/plugins/PaintTool/PaintTool.py @@ -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") diff --git a/plugins/PaintTool/PaintView.py b/plugins/PaintTool/PaintView.py index 26dd19a25b..23ef83f54b 100644 --- a/plugins/PaintTool/PaintView.py +++ b/plugins/PaintTool/PaintView.py @@ -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