diff --git a/plugins/PaintTool/PaintClearCommand.py b/plugins/PaintTool/PaintClearCommand.py new file mode 100644 index 0000000000..7ee8993bd3 --- /dev/null +++ b/plugins/PaintTool/PaintClearCommand.py @@ -0,0 +1,37 @@ +# 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 clears all the painting for the current mode""" + + def __init__(self, texture: Texture, bit_range: tuple[int, int]) -> None: + super().__init__(texture, bit_range) + + def id(self) -> int: + return 1 + + def redo(self) -> None: + cleared_image, painter = self._makeClearedTexture() + painter.end() + + self._texture.setSubImage(cleared_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 + + def _clearTextureBits(self, painter: QPainter): + painter.setCompositionMode(QPainter.CompositionMode.RasterOp_NotSourceAndDestination) + painter.fillRect(self._texture.getImage().rect(), QBrush(self._getBitRangeMask())) \ No newline at end of file diff --git a/plugins/PaintTool/PaintCommand.py b/plugins/PaintTool/PaintCommand.py new file mode 100644 index 0000000000..38f1dbc6e4 --- /dev/null +++ b/plugins/PaintTool/PaintCommand.py @@ -0,0 +1,72 @@ +# Copyright (c) 2025 UltiMaker +# Cura is released under the terms of the LGPLv3 or higher. + +from typing import Tuple, Optional + +from PyQt6.QtCore import QRect +from PyQt6.QtGui import QUndoCommand, QImage, QPainter, QBrush + +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""" + + FULL_INT32 = 0xffffffff + + def __init__(self, texture: Texture, bit_range: tuple[int, int], make_original_image = True) -> None: + super().__init__() + + self._texture: Texture = texture + self._bit_range: tuple[int, int] = bit_range + self._original_texture_image = None + 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())) + + # 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) + painter.fillRect(self._original_texture_image.rect(), QBrush(self._getBitRangeMask())) + painter.end() + + def undo(self) -> None: + if self._original_texture_image is None: + return + + cleared_image, 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()) + + def _makeClearedTexture(self) -> Tuple[QImage, QPainter]: + dest_image, painter = self._preparePainting() + self._clearTextureBits(painter) + return dest_image, painter + + def _clearTextureBits(self, painter: QPainter): + raise NotImplementedError() + + def _getBitRangeMask(self) -> int: + 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 new file mode 100644 index 0000000000..969d5d8a81 --- /dev/null +++ b/plugins/PaintTool/PaintStrokeCommand.py @@ -0,0 +1,70 @@ +# Copyright (c) 2025 UltiMaker +# Cura is released under the terms of the LGPLv3 or higher. + +from typing import cast, Optional +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 .PaintCommand import PaintCommand + +class PaintStrokeCommand(PaintCommand): + """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_path: QPainterPath, + 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._calculateBoundingRect() + self._set_value: int = set_value + self._mergeable: bool = mergeable + + def id(self) -> int: + return 0 + + def redo(self) -> None: + stroked_image, 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.end() + + self._texture.setSubImage(stroked_image, self._bounding_rect.left(), self._bounding_rect.top()) + + def mergeWith(self, command: QUndoCommand) -> bool: + if not isinstance(command, PaintStrokeCommand): + return False + paint_undo_command = cast(PaintStrokeCommand, command) + + if not paint_undo_command._mergeable: + return False + + self._stroke_path = self._stroke_path.united(paint_undo_command._stroke_path) + self._calculateBoundingRect() + + return True + + def _clearTextureBits(self, painter: QPainter): + 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) + + 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 diff --git a/plugins/PaintTool/PaintTool.py b/plugins/PaintTool/PaintTool.py index 18be2ed880..07d619fde2 100644 --- a/plugins/PaintTool/PaintTool.py +++ b/plugins/PaintTool/PaintTool.py @@ -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() @@ -184,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 @@ -434,8 +438,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 +447,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") @@ -467,7 +471,9 @@ class PaintTool(Tool): def _onSelectionChanged(self): super()._onSelectionChanged() - self.setActiveView("PaintTool" if len(Selection.getAllSelectedObjects()) == 1 else None) + single_selection = len(Selection.getAllSelectedObjects()) == 1 + self.setActiveView("PaintTool" if single_selection else None) + self._view.setCurrentPaintedObject(Selection.getSelectedObject(0) if single_selection else None) self._updateState() def _updateState(self): diff --git a/plugins/PaintTool/PaintUndoCommand.py b/plugins/PaintTool/PaintUndoCommand.py deleted file mode 100644 index 50bfb787b7..0000000000 --- a/plugins/PaintTool/PaintUndoCommand.py +++ /dev/null @@ -1,104 +0,0 @@ -# Copyright (c) 2025 UltiMaker -# Cura is released under the terms of the LGPLv3 or higher. - -from typing import cast, Optional - -from PyQt6.QtCore import QRect, QPoint -from PyQt6.QtGui import QUndoCommand, QImage, QPainter - -from UM.View.GL.Texture import Texture - - -class PaintUndoCommand(QUndoCommand): - """Provides the command that does the actual painting on objects with undo/redo mechanisms""" - - def __init__(self, - texture: Texture, - stroke_mask: QImage, - x: int, - y: int, - set_value: int, - bit_range: tuple[int, int], - mergeable: bool) -> None: - super().__init__() - - 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._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: - actual_image = self._texture.getImage() - - 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()) - painter = QPainter(stroked_image) - painter.setCompositionMode(QPainter.CompositionMode.RasterOp_SourceAndDestination) - painter.drawImage(0, 0, clear_bits_image) - painter.setCompositionMode(QPainter.CompositionMode.RasterOp_SourceOrDestination) - painter.drawImage(0, 0, set_value_image) - painter.end() - - self._texture.setSubImage(stroked_image, self._x, self._y) - - 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) - - def mergeWith(self, command: QUndoCommand) -> bool: - if not isinstance(command, PaintUndoCommand): - return False - paint_undo_command = cast(PaintUndoCommand, command) - - 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 - - return True diff --git a/plugins/PaintTool/PaintView.py b/plugins/PaintTool/PaintView.py index 0fa9cecba4..4a98fe8217 100644 --- a/plugins/PaintTool/PaintView.py +++ b/plugins/PaintTool/PaintView.py @@ -2,11 +2,13 @@ # 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 UM.Scene.SceneNode import SceneNode from cura.CuraApplication import CuraApplication from cura.BuildVolume import BuildVolume from cura.CuraView import CuraView @@ -20,7 +22,8 @@ from UM.View.GL.OpenGL import OpenGL from UM.i18n import i18nCatalog from UM.Math.Color import Color -from .PaintUndoCommand import PaintUndoCommand +from .PaintStrokeCommand import PaintStrokeCommand +from .PaintClearCommand import PaintClearCommand catalog = i18nCatalog("cura") @@ -37,31 +40,58 @@ 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._current_painted_object: Optional[SceneNode] = 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 = "" self._paint_modes: Dict[str, Dict[str, "PaintView.PaintType"]] = {} - - self._paint_undo_stack: QUndoStack = QUndoStack() - self._paint_undo_stack.setUndoLimit(32) # Set a quite low amount since every command copies the full texture - self._paint_undo_stack.canUndoChanged.connect(self.canUndoChanged) - self._paint_undo_stack.canRedoChanged.connect(self.canRedoChanged) + self._paint_undo_stacks: Dict[Tuple[SceneNode, str], QUndoStack] = {} application = CuraApplication.getInstance() application.engineCreatedSignal.connect(self._makePaintModes) self._scene = application.getController().getScene() + self._scene.getRoot().childrenChanged.connect(self._onChildrenChanged) self._extruders_model: Optional[ExtrudersModel] = None canUndoChanged = pyqtSignal(bool) canRedoChanged = pyqtSignal(bool) + def setCurrentPaintedObject(self, current_painted_object: Optional[SceneNode]): + self._current_painted_object = current_painted_object + def canUndo(self): - return self._paint_undo_stack.canUndo() + stack = self._getUndoStack() + return stack.canUndo() if stack is not None else False def canRedo(self): - return self._paint_undo_stack.canRedo() + stack = self._getUndoStack() + return stack.canRedo() if stack is not None else False + + def _getUndoStack(self): + if self._current_painted_object is None or self._current_paint_type == "": + return None + + try: + return self._paint_undo_stacks[(self._current_painted_object, self._current_paint_type)] + except KeyError: + return None + + def _onChildrenChanged(self, root_node: SceneNode): + # Gather all the actual nodes that have one or more undo stacks + stacks_keys = {} + for painted_object, paint_mode in self._paint_undo_stacks: + if painted_object in stacks_keys: + stacks_keys[painted_object].append(paint_mode) + else: + stacks_keys[painted_object] = [paint_mode] + + # Now see if any of the nodes have been deleted, i.e. they are no more linked to the root + for painted_object, paint_modes in stacks_keys.items(): + if painted_object.getDepth() == 0: + for paint_mode in paint_modes: + del self._paint_undo_stacks[(painted_object, paint_mode)] def _makePaintModes(self): application = CuraApplication.getInstance() @@ -116,81 +146,85 @@ 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() + stack = self._prepareUndoRedoStack() 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, - set_value, - (bit_range_start, bit_range_end), - merge_with_previous)) + stack.push(PaintStrokeCommand(self._current_paint_texture, + stroke_path, + set_value, + (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() + stack = self._prepareUndoRedoStack() + stack.push(PaintClearCommand(self._current_paint_texture, self._current_bits_ranges)) def undoStroke(self) -> None: - self._paint_undo_stack.undo() + stack = self._getUndoStack() + if stack is not None: + stack.undo() def redoStroke(self) -> None: - self._paint_undo_stack.redo() + stack = self._getUndoStack() + if stack is not None: + stack.redo() def getUvTexDimensions(self) -> Tuple[int, int]: if self._current_paint_texture is not None: @@ -204,6 +238,18 @@ class PaintView(CuraView): self._current_paint_type = paint_type self._prepareDataMapping() + def _prepareUndoRedoStack(self) -> QUndoStack: + stack_key = (self._current_painted_object, self._current_paint_type) + + if stack_key not in self._paint_undo_stacks: + stack: QUndoStack = QUndoStack() + stack.setUndoLimit(32) # Set a quite low amount since some commands copy the full texture + stack.canUndoChanged.connect(self.canUndoChanged) + stack.canRedoChanged.connect(self.canRedoChanged) + self._paint_undo_stacks[stack_key] = stack + + return self._paint_undo_stacks[stack_key] + def _prepareDataMapping(self): node = Selection.getAllSelectedObjects()[0] if node is None: @@ -245,16 +291,15 @@ class PaintView(CuraView): for node in Selection.getAllSelectedObjects(): paint_batch.addItem(node.getWorldTransformation(copy=False), node.getMeshData(), normal_transformation=node.getCachedNormalMatrix()) paint_texture = node.callDecoration("getPaintTexture") - if paint_texture != self._current_paint_texture: + if paint_texture != self._current_paint_texture and paint_texture is not None: self._current_paint_texture = paint_texture - self._paint_shader.setTexture(0, self._current_paint_texture) self._cursor_texture = OpenGL.getInstance().createTexture(paint_texture.getWidth(), paint_texture.getHeight()) + self._paint_shader.setTexture(0, self._current_paint_texture) image = QImage(paint_texture.getWidth(), paint_texture.getHeight(), QImage.Format.Format_ARGB32) image.fill(0) self._cursor_texture.setImage(image) self._paint_shader.setTexture(1, self._cursor_texture) - self._previous_paint_texture_stroke = None self._paint_shader.setUniformValue("u_bitsRangesStart", self._current_bits_ranges[0]) self._paint_shader.setUniformValue("u_bitsRangesEnd", self._current_bits_ranges[1])