mirror of
https://github.com/Ultimaker/Cura.git
synced 2026-03-09 21:06:57 -06:00
Merge pull request #20970 from Ultimaker/CURA-12731_fix-crash-when-loading-a-second-paint-model
Cura 12731 fix crash when loading a second paint model
This commit is contained in:
commit
c7c19c76c1
6 changed files with 295 additions and 169 deletions
37
plugins/PaintTool/PaintClearCommand.py
Normal file
37
plugins/PaintTool/PaintClearCommand.py
Normal file
|
|
@ -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()))
|
||||
72
plugins/PaintTool/PaintCommand.py
Normal file
72
plugins/PaintTool/PaintCommand.py
Normal file
|
|
@ -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
|
||||
70
plugins/PaintTool/PaintStrokeCommand.py
Normal file
70
plugins/PaintTool/PaintStrokeCommand.py
Normal file
|
|
@ -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())))
|
||||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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])
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue