mirror of
https://github.com/Ultimaker/Cura.git
synced 2026-02-15 08:59:32 -07:00
Merge pull request #20842 from Ultimaker/CURA-12661_make-more-global-paint-undo
CURA-12661 make more global paint undo
This commit is contained in:
commit
8aee04a968
8 changed files with 191 additions and 188 deletions
|
|
@ -13,27 +13,6 @@ UM.ToolbarButton
|
|||
|
||||
property string color
|
||||
|
||||
onClicked: setColor()
|
||||
|
||||
function setColor()
|
||||
{
|
||||
UM.Controller.setProperty("BrushColor", buttonBrushColor.color);
|
||||
}
|
||||
|
||||
function isChecked()
|
||||
{
|
||||
return UM.Controller.properties.getValue("BrushColor") === buttonBrushColor.color;
|
||||
}
|
||||
|
||||
Component.onCompleted:
|
||||
{
|
||||
buttonBrushColor.checked = isChecked();
|
||||
}
|
||||
|
||||
Binding
|
||||
{
|
||||
target: buttonBrushColor
|
||||
property: "checked"
|
||||
value: isChecked()
|
||||
}
|
||||
checked: UM.Controller.properties.getValue("BrushColor") === buttonBrushColor.color
|
||||
onClicked: UM.Controller.setProperty("BrushColor", buttonBrushColor.color)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,27 +13,6 @@ UM.ToolbarButton
|
|||
|
||||
property int shape
|
||||
|
||||
onClicked: setShape()
|
||||
|
||||
function setShape()
|
||||
{
|
||||
UM.Controller.setProperty("BrushShape", buttonBrushShape.shape)
|
||||
}
|
||||
|
||||
function isChecked()
|
||||
{
|
||||
return UM.Controller.properties.getValue("BrushShape") === buttonBrushShape.shape;
|
||||
}
|
||||
|
||||
Component.onCompleted:
|
||||
{
|
||||
buttonBrushShape.checked = isChecked();
|
||||
}
|
||||
|
||||
Binding
|
||||
{
|
||||
target: buttonBrushShape
|
||||
property: "checked"
|
||||
value: isChecked()
|
||||
}
|
||||
checked: UM.Controller.properties.getValue("BrushShape") === buttonBrushShape.shape
|
||||
onClicked: UM.Controller.setProperty("BrushShape", buttonBrushShape.shape)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,27 +13,6 @@ Cura.ModeSelectorButton
|
|||
|
||||
property string mode
|
||||
|
||||
onClicked: setMode()
|
||||
|
||||
function setMode()
|
||||
{
|
||||
UM.Controller.setProperty("PaintType", modeSelectorButton.mode);
|
||||
}
|
||||
|
||||
function isSelected()
|
||||
{
|
||||
return UM.Controller.properties.getValue("PaintType") === modeSelectorButton.mode;
|
||||
}
|
||||
|
||||
Component.onCompleted:
|
||||
{
|
||||
modeSelectorButton.selected = isSelected();
|
||||
}
|
||||
|
||||
Binding
|
||||
{
|
||||
target: modeSelectorButton
|
||||
property: "selected"
|
||||
value: isSelected()
|
||||
}
|
||||
selected: UM.Controller.properties.getValue("PaintType") === modeSelectorButton.mode
|
||||
onClicked: UM.Controller.setProperty("PaintType", modeSelectorButton.mode)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,9 +42,13 @@ class PaintTool(Tool):
|
|||
PREPARING_MODEL = 1 # Model is being prepared (UV-unwrapping, texture generation)
|
||||
READY = 2 # Ready to paint !
|
||||
|
||||
def __init__(self) -> None:
|
||||
def __init__(self, view: PaintView) -> None:
|
||||
super().__init__()
|
||||
|
||||
self._view: PaintView = view
|
||||
self._view.canUndoChanged.connect(self._onCanUndoChanged)
|
||||
self._view.canRedoChanged.connect(self._onCanRedoChanged)
|
||||
|
||||
self._picking_pass: Optional[PickingPass] = None
|
||||
self._faces_selection_pass: Optional[SelectionPass] = None
|
||||
|
||||
|
|
@ -68,7 +72,7 @@ class PaintTool(Tool):
|
|||
self._state: PaintTool.Paint.State = PaintTool.Paint.State.MULTIPLE_SELECTION
|
||||
self._prepare_texture_job: Optional[PrepareTextureJob] = None
|
||||
|
||||
self.setExposedProperties("PaintType", "BrushSize", "BrushColor", "BrushShape", "State")
|
||||
self.setExposedProperties("PaintType", "BrushSize", "BrushColor", "BrushShape", "State", "CanUndo", "CanRedo")
|
||||
|
||||
self._controller.activeViewChanged.connect(self._updateIgnoreUnselectedObjects)
|
||||
self._controller.activeToolChanged.connect(self._updateState)
|
||||
|
|
@ -108,19 +112,11 @@ class PaintTool(Tool):
|
|||
return stroke_image, (start_x, start_y)
|
||||
|
||||
def getPaintType(self) -> str:
|
||||
paint_view = self._get_paint_view()
|
||||
if paint_view is None:
|
||||
return ""
|
||||
|
||||
return paint_view.getPaintType()
|
||||
return self._view.getPaintType()
|
||||
|
||||
def setPaintType(self, paint_type: str) -> None:
|
||||
paint_view = self._get_paint_view()
|
||||
if paint_view is None:
|
||||
return
|
||||
|
||||
if paint_type != self.getPaintType():
|
||||
paint_view.setPaintType(paint_type)
|
||||
self._view.setPaintType(paint_type)
|
||||
|
||||
self._brush_pen = self._createBrushPen()
|
||||
self._updateScene()
|
||||
|
|
@ -153,41 +149,37 @@ class PaintTool(Tool):
|
|||
self._brush_pen = self._createBrushPen()
|
||||
self.propertyChanged.emit()
|
||||
|
||||
def getCanUndo(self) -> bool:
|
||||
return self._view.canUndo()
|
||||
|
||||
def getState(self) -> int:
|
||||
return self._state
|
||||
|
||||
def undoStackAction(self, redo_instead: bool) -> bool:
|
||||
paint_view = self._get_paint_view()
|
||||
if paint_view is None:
|
||||
return False
|
||||
def _onCanUndoChanged(self):
|
||||
self.propertyChanged.emit()
|
||||
|
||||
if redo_instead:
|
||||
paint_view.redoStroke()
|
||||
else:
|
||||
paint_view.undoStroke()
|
||||
def getCanRedo(self) -> bool:
|
||||
return self._view.canRedo()
|
||||
|
||||
def _onCanRedoChanged(self):
|
||||
self.propertyChanged.emit()
|
||||
|
||||
def undoStackAction(self) -> None:
|
||||
self._view.undoStroke()
|
||||
self._updateScene()
|
||||
|
||||
def redoStackAction(self) -> None:
|
||||
self._view.redoStroke()
|
||||
self._updateScene()
|
||||
return True
|
||||
|
||||
def clear(self) -> None:
|
||||
paintview = self._get_paint_view()
|
||||
if paintview is None:
|
||||
return
|
||||
|
||||
width, height = paintview.getUvTexDimensions()
|
||||
width, height = self._view.getUvTexDimensions()
|
||||
clear_image = QImage(width, height, QImage.Format.Format_RGB32)
|
||||
clear_image.fill(Qt.GlobalColor.white)
|
||||
paintview.addStroke(clear_image, 0, 0, "none")
|
||||
self._view.addStroke(clear_image, 0, 0, "none", False)
|
||||
|
||||
self._updateScene()
|
||||
|
||||
@staticmethod
|
||||
def _get_paint_view() -> Optional[PaintView]:
|
||||
paint_view = Application.getInstance().getController().getActiveView()
|
||||
if paint_view is None or paint_view.getPluginId() != "PaintTool":
|
||||
return None
|
||||
return cast(PaintView, paint_view)
|
||||
|
||||
@staticmethod
|
||||
def _get_intersect_ratio_via_pt(a: numpy.ndarray, pt: numpy.ndarray, b: numpy.ndarray, c: numpy.ndarray) -> float:
|
||||
# compute the intersection of (param) A - pt with (param) B - (param) C
|
||||
|
|
@ -327,10 +319,6 @@ class PaintTool(Tool):
|
|||
else:
|
||||
self._mouse_held = True
|
||||
|
||||
paintview = self._get_paint_view()
|
||||
if paintview is None:
|
||||
return False
|
||||
|
||||
if not self._faces_selection_pass:
|
||||
self._faces_selection_pass = CuraApplication.getInstance().getRenderer().getRenderPass("selection_faces")
|
||||
if not self._faces_selection_pass:
|
||||
|
|
@ -373,7 +361,7 @@ class PaintTool(Tool):
|
|||
(self._last_mouse_coords, (self._last_face_id, self._last_text_coords)),
|
||||
((mouse_evt.x, mouse_evt.y), (face_id, texcoords)))
|
||||
|
||||
w, h = paintview.getUvTexDimensions()
|
||||
w, h = self._view.getUvTexDimensions()
|
||||
for start_coords, end_coords in substrokes:
|
||||
sub_image, (start_x, start_y) = self._createStrokeImage(
|
||||
start_coords[0] * w,
|
||||
|
|
@ -381,7 +369,7 @@ class PaintTool(Tool):
|
|||
end_coords[0] * w,
|
||||
end_coords[1] * h
|
||||
)
|
||||
paintview.addStroke(sub_image, start_x, start_y, self._brush_color)
|
||||
self._view.addStroke(sub_image, start_x, start_y, self._brush_color, is_moved)
|
||||
|
||||
self._last_text_coords = texcoords
|
||||
self._last_mouse_coords = (mouse_evt.x, mouse_evt.y)
|
||||
|
|
|
|||
|
|
@ -19,14 +19,16 @@ Item
|
|||
{
|
||||
id: undoAction
|
||||
shortcut: "Ctrl+L"
|
||||
onTriggered: UM.Controller.triggerActionWithData("undoStackAction", false)
|
||||
enabled: UM.Controller.properties.getValue("CanUndo")
|
||||
onTriggered: UM.Controller.triggerAction("undoStackAction")
|
||||
}
|
||||
|
||||
Action
|
||||
{
|
||||
id: redoAction
|
||||
shortcut: "Ctrl+Shift+L"
|
||||
onTriggered: UM.Controller.triggerActionWithData("undoStackAction", true)
|
||||
enabled: UM.Controller.properties.getValue("CanRedo")
|
||||
onTriggered: UM.Controller.triggerAction("redoStackAction")
|
||||
}
|
||||
|
||||
Column
|
||||
|
|
@ -163,6 +165,7 @@ Item
|
|||
|
||||
from: 10
|
||||
to: 1000
|
||||
value: UM.Controller.properties.getValue("BrushSize")
|
||||
|
||||
onPressedChanged: function(pressed)
|
||||
{
|
||||
|
|
@ -171,11 +174,6 @@ Item
|
|||
UM.Controller.setProperty("BrushSize", shapeSizeSlider.value);
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted:
|
||||
{
|
||||
shapeSizeSlider.value = UM.Controller.properties.getValue("BrushSize");
|
||||
}
|
||||
}
|
||||
|
||||
//Line between the sections.
|
||||
|
|
@ -192,6 +190,7 @@ Item
|
|||
{
|
||||
id: undoButton
|
||||
|
||||
enabled: undoAction.enabled
|
||||
text: catalog.i18nc("@action:button", "Undo Stroke")
|
||||
toolItem: UM.ColorImage
|
||||
{
|
||||
|
|
@ -206,6 +205,7 @@ Item
|
|||
{
|
||||
id: redoButton
|
||||
|
||||
enabled: redoAction.enabled
|
||||
text: catalog.i18nc("@action:button", "Redo Stroke")
|
||||
toolItem: UM.ColorImage
|
||||
{
|
||||
|
|
|
|||
104
plugins/PaintTool/PaintUndoCommand.py
Normal file
104
plugins/PaintTool/PaintUndoCommand.py
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
# 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,10 +2,10 @@
|
|||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import os
|
||||
from PyQt6.QtCore import QRect
|
||||
from typing import Optional, List, Tuple, Dict, cast
|
||||
from PyQt6.QtCore import QRect, pyqtSignal
|
||||
from typing import Optional, Dict
|
||||
|
||||
from PyQt6.QtGui import QImage, QColor, QPainter
|
||||
from PyQt6.QtGui import QImage, QUndoStack
|
||||
|
||||
from cura.CuraApplication import CuraApplication
|
||||
from cura.BuildVolume import BuildVolume
|
||||
|
|
@ -19,14 +19,14 @@ from UM.View.GL.OpenGL import OpenGL
|
|||
from UM.i18n import i18nCatalog
|
||||
from UM.Math.Color import Color
|
||||
|
||||
from .PaintUndoCommand import PaintUndoCommand
|
||||
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
|
||||
class PaintView(CuraView):
|
||||
"""View for model-painting."""
|
||||
|
||||
UNDO_STACK_SIZE = 1024
|
||||
|
||||
class PaintType:
|
||||
def __init__(self, display_color: Color, value: int):
|
||||
self.display_color: Color = display_color
|
||||
|
|
@ -40,16 +40,24 @@ class PaintView(CuraView):
|
|||
self._current_paint_type = ""
|
||||
self._paint_modes: Dict[str, Dict[str, "PaintView.PaintType"]] = {}
|
||||
|
||||
self._stroke_undo_stack: List[Tuple[QImage, int, int]] = []
|
||||
self._stroke_redo_stack: List[Tuple[QImage, int, int]] = []
|
||||
|
||||
self._force_opaque_mask = QImage(2, 2, QImage.Format.Format_Mono)
|
||||
self._force_opaque_mask.fill(1)
|
||||
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)
|
||||
|
||||
application = CuraApplication.getInstance()
|
||||
application.engineCreatedSignal.connect(self._makePaintModes)
|
||||
self._scene = application.getController().getScene()
|
||||
|
||||
canUndoChanged = pyqtSignal(bool)
|
||||
canRedoChanged = pyqtSignal(bool)
|
||||
|
||||
def canUndo(self):
|
||||
return self._paint_undo_stack.canUndo()
|
||||
|
||||
def canRedo(self):
|
||||
return self._paint_undo_stack.canRedo()
|
||||
|
||||
def _makePaintModes(self):
|
||||
theme = CuraApplication.getInstance().getTheme()
|
||||
usual_types = {"none": self.PaintType(Color(*theme.getColor("paint_normal_area").getRgb()), 0),
|
||||
|
|
@ -67,76 +75,41 @@ class PaintView(CuraView):
|
|||
shader_filename = os.path.join(PluginRegistry.getInstance().getPluginPath("PaintTool"), "paint.shader")
|
||||
self._paint_shader = OpenGL.getInstance().createShaderProgram(shader_filename)
|
||||
|
||||
def _forceOpaqueDeepCopy(self, image: QImage):
|
||||
res = QImage(image.width(), image.height(), QImage.Format.Format_RGBA8888)
|
||||
res.fill(QColor(255, 255, 255, 255))
|
||||
painter = QPainter(res)
|
||||
painter.setRenderHint(QPainter.RenderHint.Antialiasing, False)
|
||||
painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_SourceOver)
|
||||
painter.drawImage(0, 0, image)
|
||||
painter.end()
|
||||
res.setAlphaChannel(self._force_opaque_mask.scaled(image.width(), image.height()))
|
||||
return res
|
||||
|
||||
def addStroke(self, stroke_mask: QImage, start_x: int, start_y: int, brush_color: str) -> None:
|
||||
def addStroke(self, stroke_mask: QImage, start_x: int, start_y: int, 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()
|
||||
|
||||
actual_image = self._current_paint_texture.getImage()
|
||||
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()
|
||||
|
||||
bit_range_start, bit_range_end = self._current_bits_ranges
|
||||
set_value = self._paint_modes[self._current_paint_type][brush_color].value << self._current_bits_ranges[0]
|
||||
full_int32 = 0xffffffff
|
||||
clear_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, stroke_mask.width(), stroke_mask.height())
|
||||
set_value = self._paint_modes[self._current_paint_type][brush_color].value << bit_range_start
|
||||
|
||||
clear_bits_image = stroke_mask.copy()
|
||||
clear_bits_image.invertPixels()
|
||||
painter = QPainter(clear_bits_image)
|
||||
painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_Lighten)
|
||||
painter.fillRect(image_rect, clear_mask)
|
||||
painter.end()
|
||||
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))
|
||||
|
||||
set_value_image = stroke_mask.copy()
|
||||
painter = QPainter(set_value_image)
|
||||
painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_Multiply)
|
||||
painter.fillRect(image_rect, set_value)
|
||||
painter.end()
|
||||
def undoStroke(self) -> None:
|
||||
self._paint_undo_stack.undo()
|
||||
|
||||
stroked_image = actual_image.copy(start_x, start_y, stroke_mask.width(), 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._stroke_redo_stack.clear()
|
||||
if len(self._stroke_undo_stack) >= PaintView.UNDO_STACK_SIZE:
|
||||
self._stroke_undo_stack.pop(0)
|
||||
undo_image = self._forceOpaqueDeepCopy(self._current_paint_texture.setSubImage(stroked_image, start_x, start_y))
|
||||
if undo_image is not None:
|
||||
self._stroke_undo_stack.append((undo_image, start_x, start_y))
|
||||
|
||||
def _applyUndoStacksAction(self, from_stack: List[Tuple[QImage, int, int]], to_stack: List[Tuple[QImage, int, int]]) -> bool:
|
||||
if len(from_stack) <= 0 or self._current_paint_texture is None:
|
||||
return False
|
||||
from_image, x, y = from_stack.pop()
|
||||
to_image = self._forceOpaqueDeepCopy(self._current_paint_texture.setSubImage(from_image, x, y))
|
||||
if to_image is None:
|
||||
return False
|
||||
if len(to_stack) >= PaintView.UNDO_STACK_SIZE:
|
||||
to_stack.pop(0)
|
||||
to_stack.append((to_image, x, y))
|
||||
return True
|
||||
|
||||
def undoStroke(self) -> bool:
|
||||
return self._applyUndoStacksAction(self._stroke_undo_stack, self._stroke_redo_stack)
|
||||
|
||||
def redoStroke(self) -> bool:
|
||||
return self._applyUndoStacksAction(self._stroke_redo_stack, self._stroke_undo_stack)
|
||||
def redoStroke(self) -> None:
|
||||
self._paint_undo_stack.redo()
|
||||
|
||||
def getUvTexDimensions(self):
|
||||
if self._current_paint_texture is not None:
|
||||
|
|
|
|||
|
|
@ -28,7 +28,8 @@ def getMetaData():
|
|||
def register(app):
|
||||
qmlRegisterUncreatableType(PaintTool.PaintTool.Brush, "Cura", 1, 0, "This is an enumeration class", "PaintToolBrush")
|
||||
qmlRegisterUncreatableType(PaintTool.PaintTool.Paint, "Cura", 1, 0, "This is an enumeration class", "PaintToolState")
|
||||
view = PaintView.PaintView()
|
||||
return {
|
||||
"tool": PaintTool.PaintTool(),
|
||||
"view": PaintView.PaintView()
|
||||
"tool": PaintTool.PaintTool(view),
|
||||
"view": view
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue