Merge pull request #20970 from Ultimaker/CURA-12731_fix-crash-when-loading-a-second-paint-model
Some checks failed
unit-test / Run unit tests (push) Waiting to run
conan-package / conan-package (push) Has been cancelled

Cura 12731 fix crash when loading a second paint model
This commit is contained in:
HellAholic 2025-09-23 08:34:41 +00:00 committed by GitHub
commit c7c19c76c1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 295 additions and 169 deletions

View 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()))

View 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

View 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())))

View file

@ -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):

View file

@ -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

View file

@ -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])