mirror of
https://github.com/Ultimaker/Cura.git
synced 2026-02-15 17:09:33 -07:00
Proper paint-on-seam UI
CURA-12578
This commit is contained in:
parent
a1d1dc2ea0
commit
bbddcab4e9
13 changed files with 274 additions and 165 deletions
25
plugins/PaintTool/BrushColorButton.qml
Normal file
25
plugins/PaintTool/BrushColorButton.qml
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
// Copyright (c) 2025 UltiMaker
|
||||
// Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import QtQuick
|
||||
|
||||
import UM 1.7 as UM
|
||||
import Cura 1.0 as Cura
|
||||
|
||||
|
||||
UM.ToolbarButton
|
||||
{
|
||||
id: buttonBrushColor
|
||||
|
||||
property string color
|
||||
|
||||
checked: base.selectedColor === buttonBrushColor.color
|
||||
|
||||
onClicked: setColor()
|
||||
|
||||
function setColor()
|
||||
{
|
||||
base.selectedColor = buttonBrushColor.color
|
||||
UM.Controller.triggerActionWithData("setBrushColor", buttonBrushColor.color)
|
||||
}
|
||||
}
|
||||
25
plugins/PaintTool/BrushShapeButton.qml
Normal file
25
plugins/PaintTool/BrushShapeButton.qml
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
// Copyright (c) 2025 UltiMaker
|
||||
// Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import QtQuick
|
||||
|
||||
import UM 1.7 as UM
|
||||
import Cura 1.0 as Cura
|
||||
|
||||
|
||||
UM.ToolbarButton
|
||||
{
|
||||
id: buttonBrushShape
|
||||
|
||||
property int shape
|
||||
|
||||
checked: base.selectedShape === buttonBrushShape.shape
|
||||
|
||||
onClicked: setShape()
|
||||
|
||||
function setShape()
|
||||
{
|
||||
base.selectedShape = buttonBrushShape.shape
|
||||
UM.Controller.triggerActionWithData("setBrushShape", buttonBrushShape.shape)
|
||||
}
|
||||
}
|
||||
24
plugins/PaintTool/PaintModeButton.qml
Normal file
24
plugins/PaintTool/PaintModeButton.qml
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
// Copyright (c) 2025 UltiMaker
|
||||
// Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import QtQuick
|
||||
|
||||
import UM 1.7 as UM
|
||||
import Cura 1.0 as Cura
|
||||
|
||||
Cura.ModeSelectorButton
|
||||
{
|
||||
id: modeSelectorButton
|
||||
|
||||
property string mode
|
||||
|
||||
selected: base.selectedMode === modeSelectorButton.mode
|
||||
|
||||
onClicked: setMode()
|
||||
|
||||
function setMode()
|
||||
{
|
||||
base.selectedMode = modeSelectorButton.mode
|
||||
UM.Controller.triggerActionWithData("setPaintType", modeSelectorButton.mode)
|
||||
}
|
||||
}
|
||||
|
|
@ -42,7 +42,7 @@ class PaintTool(Tool):
|
|||
self._cache_dirty: bool = True
|
||||
|
||||
self._brush_size: int = 10
|
||||
self._brush_color: str = "A"
|
||||
self._brush_color: str = ""
|
||||
self._brush_shape: PaintTool.Brush.Shape = PaintTool.Brush.Shape.SQUARE
|
||||
self._brush_pen: QPen = self._createBrushPen()
|
||||
|
||||
|
|
@ -122,6 +122,18 @@ class PaintTool(Tool):
|
|||
self._updateScene()
|
||||
return True
|
||||
|
||||
def clear(self) -> None:
|
||||
paintview = self._get_paint_view()
|
||||
if paintview is None:
|
||||
return
|
||||
|
||||
width, height = paintview.getUvTexDimensions()
|
||||
clear_image = QImage(width, height, QImage.Format.Format_RGB32)
|
||||
clear_image.fill(Qt.GlobalColor.white)
|
||||
paintview.addStroke(clear_image, 0, 0, "none")
|
||||
|
||||
self._updateScene()
|
||||
|
||||
@staticmethod
|
||||
def _get_paint_view() -> Optional[PaintView]:
|
||||
paint_view = Application.getInstance().getController().getActiveView()
|
||||
|
|
@ -265,10 +277,9 @@ class PaintTool(Tool):
|
|||
else:
|
||||
self._mouse_held = True
|
||||
|
||||
paintview = controller.getActiveView()
|
||||
if paintview is None or paintview.getPluginId() != "PaintTool":
|
||||
paintview = self._get_paint_view()
|
||||
if paintview is None:
|
||||
return False
|
||||
paintview = cast(PaintView, paintview)
|
||||
|
||||
if not self._selection_pass:
|
||||
return False
|
||||
|
|
|
|||
|
|
@ -15,6 +15,10 @@ Item
|
|||
height: childrenRect.height
|
||||
UM.I18nCatalog { id: catalog; name: "cura"}
|
||||
|
||||
property string selectedMode: ""
|
||||
property string selectedColor: ""
|
||||
property int selectedShape: 0
|
||||
|
||||
Action
|
||||
{
|
||||
id: undoAction
|
||||
|
|
@ -29,170 +33,158 @@ Item
|
|||
onTriggered: UM.Controller.triggerActionWithData("undoStackAction", true)
|
||||
}
|
||||
|
||||
ColumnLayout
|
||||
Column
|
||||
{
|
||||
id: mainColumn
|
||||
spacing: UM.Theme.getSize("default_margin").height
|
||||
|
||||
RowLayout
|
||||
{
|
||||
UM.ToolbarButton
|
||||
id: rowPaintMode
|
||||
width: parent.width
|
||||
|
||||
PaintModeButton
|
||||
{
|
||||
id: paintTypeA
|
||||
|
||||
text: catalog.i18nc("@action:button", "Paint Type A")
|
||||
toolItem: UM.ColorImage
|
||||
{
|
||||
source: UM.Theme.getIcon("Buildplate")
|
||||
color: UM.Theme.getColor("icon")
|
||||
}
|
||||
property bool needBorder: true
|
||||
|
||||
z: 2
|
||||
|
||||
onClicked: UM.Controller.triggerActionWithData("setPaintType", "A")
|
||||
text: catalog.i18nc("@action:button", "Seam")
|
||||
icon: "Seam"
|
||||
tooltipText: catalog.i18nc("@tooltip", "Refine seam placement by defining preferred/avoidance areas")
|
||||
mode: "seam"
|
||||
}
|
||||
|
||||
UM.ToolbarButton
|
||||
PaintModeButton
|
||||
{
|
||||
id: paintTypeB
|
||||
text: catalog.i18nc("@action:button", "Support")
|
||||
icon: "Support"
|
||||
tooltipText: catalog.i18nc("@tooltip", "Refine support placement by defining preferred/avoidance areas")
|
||||
mode: "support"
|
||||
}
|
||||
}
|
||||
|
||||
text: catalog.i18nc("@action:button", "Paint Type B")
|
||||
//Line between the sections.
|
||||
Rectangle
|
||||
{
|
||||
width: parent.width
|
||||
height: UM.Theme.getSize("default_lining").height
|
||||
color: UM.Theme.getColor("lining")
|
||||
}
|
||||
|
||||
RowLayout
|
||||
{
|
||||
id: rowBrushColor
|
||||
|
||||
UM.Label
|
||||
{
|
||||
text: catalog.i18nc("@label", "Mark as")
|
||||
}
|
||||
|
||||
BrushColorButton
|
||||
{
|
||||
id: buttonPreferredArea
|
||||
color: "preferred"
|
||||
|
||||
text: catalog.i18nc("@action:button", "Preferred")
|
||||
toolItem: UM.ColorImage
|
||||
{
|
||||
source: UM.Theme.getIcon("BlackMagic")
|
||||
source: UM.Theme.getIcon("CheckBadge", "low")
|
||||
color: UM.Theme.getColor("paint_preferred_area")
|
||||
}
|
||||
}
|
||||
|
||||
BrushColorButton
|
||||
{
|
||||
id: buttonAvoidArea
|
||||
color: "avoid"
|
||||
|
||||
text: catalog.i18nc("@action:button", "Avoid")
|
||||
toolItem: UM.ColorImage
|
||||
{
|
||||
source: UM.Theme.getIcon("CancelBadge", "low")
|
||||
color: UM.Theme.getColor("paint_avoid_area")
|
||||
}
|
||||
}
|
||||
|
||||
BrushColorButton
|
||||
{
|
||||
id: buttonEraseArea
|
||||
color: "none"
|
||||
|
||||
text: catalog.i18nc("@action:button", "Erase")
|
||||
toolItem: UM.ColorImage
|
||||
{
|
||||
source: UM.Theme.getIcon("Eraser")
|
||||
color: UM.Theme.getColor("icon")
|
||||
}
|
||||
property bool needBorder: true
|
||||
|
||||
z: 2
|
||||
|
||||
onClicked: UM.Controller.triggerActionWithData("setPaintType", "B")
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout
|
||||
{
|
||||
UM.ToolbarButton
|
||||
id: rowBrushShape
|
||||
|
||||
UM.Label
|
||||
{
|
||||
id: colorButtonA
|
||||
|
||||
text: catalog.i18nc("@action:button", "Color A")
|
||||
toolItem: UM.ColorImage
|
||||
{
|
||||
source: UM.Theme.getIcon("Eye")
|
||||
color: "purple"
|
||||
}
|
||||
property bool needBorder: true
|
||||
|
||||
z: 2
|
||||
|
||||
onClicked: UM.Controller.triggerActionWithData("setBrushColor", "A")
|
||||
text: catalog.i18nc("@label", "Brush Shape")
|
||||
}
|
||||
|
||||
UM.ToolbarButton
|
||||
BrushShapeButton
|
||||
{
|
||||
id: colorButtonB
|
||||
id: buttonBrushCircle
|
||||
shape: Cura.PaintToolBrush.CIRCLE
|
||||
|
||||
text: catalog.i18nc("@action:button", "Color B")
|
||||
text: catalog.i18nc("@action:button", "Circle")
|
||||
toolItem: UM.ColorImage
|
||||
{
|
||||
source: UM.Theme.getIcon("Eye")
|
||||
color: "orange"
|
||||
source: UM.Theme.getIcon("Circle")
|
||||
color: UM.Theme.getColor("icon")
|
||||
}
|
||||
property bool needBorder: true
|
||||
|
||||
z: 2
|
||||
|
||||
onClicked: UM.Controller.triggerActionWithData("setBrushColor", "B")
|
||||
}
|
||||
|
||||
UM.ToolbarButton
|
||||
BrushShapeButton
|
||||
{
|
||||
id: colorButtonC
|
||||
id: buttonBrushSquare
|
||||
shape: Cura.PaintToolBrush.SQUARE
|
||||
|
||||
text: catalog.i18nc("@action:button", "Color C")
|
||||
toolItem: UM.ColorImage
|
||||
{
|
||||
source: UM.Theme.getIcon("Eye")
|
||||
color: "green"
|
||||
}
|
||||
property bool needBorder: true
|
||||
|
||||
z: 2
|
||||
|
||||
onClicked: UM.Controller.triggerActionWithData("setBrushColor", "C")
|
||||
}
|
||||
|
||||
UM.ToolbarButton
|
||||
{
|
||||
id: colorButtonD
|
||||
|
||||
text: catalog.i18nc("@action:button", "Color D")
|
||||
toolItem: UM.ColorImage
|
||||
{
|
||||
source: UM.Theme.getIcon("Eye")
|
||||
color: "ghostwhite"
|
||||
}
|
||||
property bool needBorder: true
|
||||
|
||||
z: 2
|
||||
|
||||
onClicked: UM.Controller.triggerActionWithData("setBrushColor", "D")
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout
|
||||
{
|
||||
UM.ToolbarButton
|
||||
{
|
||||
id: shapeSquareButton
|
||||
|
||||
text: catalog.i18nc("@action:button", "Square Brush")
|
||||
text: catalog.i18nc("@action:button", "Square")
|
||||
toolItem: UM.ColorImage
|
||||
{
|
||||
source: UM.Theme.getIcon("MeshTypeNormal")
|
||||
color: UM.Theme.getColor("icon")
|
||||
}
|
||||
property bool needBorder: true
|
||||
|
||||
z: 2
|
||||
|
||||
onClicked: UM.Controller.triggerActionWithData("setBrushShape", Cura.PaintToolBrush.SQUARE)
|
||||
}
|
||||
}
|
||||
|
||||
UM.ToolbarButton
|
||||
UM.Label
|
||||
{
|
||||
text: catalog.i18nc("@label", "Brush Size")
|
||||
}
|
||||
|
||||
UM.Slider
|
||||
{
|
||||
id: shapeSizeSlider
|
||||
width: parent.width
|
||||
indicatorVisible: false
|
||||
|
||||
from: 1
|
||||
to: 40
|
||||
value: 10
|
||||
|
||||
onPressedChanged: function(pressed)
|
||||
{
|
||||
id: shapeCircleButton
|
||||
|
||||
text: catalog.i18nc("@action:button", "Round Brush")
|
||||
toolItem: UM.ColorImage
|
||||
if(! pressed)
|
||||
{
|
||||
source: UM.Theme.getIcon("CircleOutline")
|
||||
color: UM.Theme.getColor("icon")
|
||||
}
|
||||
property bool needBorder: true
|
||||
|
||||
z: 2
|
||||
|
||||
onClicked: UM.Controller.triggerActionWithData("setBrushShape", Cura.PaintToolBrush.CIRCLE)
|
||||
}
|
||||
|
||||
UM.Slider
|
||||
{
|
||||
id: shapeSizeSlider
|
||||
|
||||
from: 1
|
||||
to: 40
|
||||
value: 10
|
||||
|
||||
onPressedChanged: function(pressed)
|
||||
{
|
||||
if(! pressed)
|
||||
{
|
||||
UM.Controller.triggerActionWithData("setBrushSize", shapeSizeSlider.value)
|
||||
}
|
||||
UM.Controller.triggerActionWithData("setBrushSize", shapeSizeSlider.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Line between the sections.
|
||||
Rectangle
|
||||
{
|
||||
width: parent.width
|
||||
height: UM.Theme.getSize("default_lining").height
|
||||
color: UM.Theme.getColor("lining")
|
||||
}
|
||||
|
||||
RowLayout
|
||||
{
|
||||
UM.ToolbarButton
|
||||
|
|
@ -203,10 +195,8 @@ Item
|
|||
toolItem: UM.ColorImage
|
||||
{
|
||||
source: UM.Theme.getIcon("ArrowReset")
|
||||
color: UM.Theme.getColor("icon")
|
||||
}
|
||||
property bool needBorder: true
|
||||
|
||||
z: 2
|
||||
|
||||
onClicked: undoAction.trigger()
|
||||
}
|
||||
|
|
@ -218,14 +208,30 @@ Item
|
|||
text: catalog.i18nc("@action:button", "Redo Stroke")
|
||||
toolItem: UM.ColorImage
|
||||
{
|
||||
source: UM.Theme.getIcon("ArrowDoubleCircleRight")
|
||||
source: UM.Theme.getIcon("ArrowReset")
|
||||
color: UM.Theme.getColor("icon")
|
||||
transform: [
|
||||
Scale { xScale: -1; origin.x: width/2 }
|
||||
]
|
||||
}
|
||||
property bool needBorder: true
|
||||
|
||||
z: 2
|
||||
|
||||
onClicked: redoAction.trigger()
|
||||
}
|
||||
|
||||
Cura.SecondaryButton
|
||||
{
|
||||
id: clearButton
|
||||
text: catalog.i18nc("@button", "Clear all")
|
||||
onClicked: UM.Controller.triggerAction("clear")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted:
|
||||
{
|
||||
// Force first types for consistency, otherwise UI may become different from controller
|
||||
rowPaintMode.children[0].setMode()
|
||||
rowBrushColor.children[1].setColor()
|
||||
rowBrushShape.children[1].setShape()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,23 +26,17 @@ class PaintView(View):
|
|||
UNDO_STACK_SIZE = 1024
|
||||
|
||||
class PaintType:
|
||||
def __init__(self, icon: str, display_color: Color, value: int):
|
||||
self.icon: str = icon
|
||||
def __init__(self, display_color: Color, value: int):
|
||||
self.display_color: Color = display_color
|
||||
self.value: int = value
|
||||
|
||||
class PaintMode:
|
||||
def __init__(self, icon: str, types: Dict[str, "PaintView.PaintType"]):
|
||||
self.icon: str = icon
|
||||
self.types = types
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self._paint_shader: Optional[ShaderProgram] = None
|
||||
self._current_paint_texture: Optional[Texture] = None
|
||||
self._current_bits_ranges: tuple[int, int] = (0, 0)
|
||||
self._current_paint_type = ""
|
||||
self._paint_modes: Dict[str, PaintView.PaintMode] = {}
|
||||
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]] = []
|
||||
|
|
@ -54,12 +48,12 @@ class PaintView(View):
|
|||
|
||||
def _makePaintModes(self):
|
||||
theme = CuraApplication.getInstance().getTheme()
|
||||
usual_types = {"A": self.PaintType("Buildplate", Color(*theme.getColor("paint_normal_area").getRgb()), 0),
|
||||
"B": self.PaintType("BlackMagic", Color(*theme.getColor("paint_preferred_area").getRgb()), 1),
|
||||
"C": self.PaintType("Eye", Color(*theme.getColor("paint_avoid_area").getRgb()), 2)}
|
||||
usual_types = {"none": self.PaintType(Color(*theme.getColor("paint_normal_area").getRgb()), 0),
|
||||
"preferred": self.PaintType(Color(*theme.getColor("paint_preferred_area").getRgb()), 1),
|
||||
"avoid": self.PaintType(Color(*theme.getColor("paint_avoid_area").getRgb()), 2)}
|
||||
self._paint_modes = {
|
||||
"A": self.PaintMode("MeshTypeNormal", usual_types),
|
||||
"B": self.PaintMode("CircleOutline", usual_types),
|
||||
"seam": usual_types,
|
||||
"support": usual_types,
|
||||
}
|
||||
|
||||
def _checkSetup(self):
|
||||
|
|
@ -78,32 +72,32 @@ class PaintView(View):
|
|||
res.setAlphaChannel(self._force_opaque_mask.scaled(image.width(), image.height()))
|
||||
return res
|
||||
|
||||
def addStroke(self, stroke_image: 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) -> None:
|
||||
if self._current_paint_texture is None or self._current_paint_texture.getImage() is None:
|
||||
return
|
||||
|
||||
actual_image = self._current_paint_texture.getImage()
|
||||
|
||||
bit_range_start, bit_range_end = self._current_bits_ranges
|
||||
set_value = self._paint_modes[self._current_paint_type].types[brush_color].value << self._current_bits_ranges[0]
|
||||
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_image.width(), stroke_image.height())
|
||||
image_rect = QRect(0, 0, stroke_mask.width(), stroke_mask.height())
|
||||
|
||||
clear_bits_image = stroke_image.copy()
|
||||
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()
|
||||
|
||||
set_value_image = stroke_image.copy()
|
||||
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()
|
||||
|
||||
stroked_image = actual_image.copy(start_x, start_y, stroke_image.width(), stroke_image.height())
|
||||
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)
|
||||
|
|
@ -149,7 +143,7 @@ class PaintView(View):
|
|||
paint_data_mapping = node.callDecoration("getTextureDataMapping")
|
||||
|
||||
if paint_type not in paint_data_mapping:
|
||||
new_mapping = self._add_mapping(paint_data_mapping, len(self._paint_modes[paint_type].types))
|
||||
new_mapping = self._add_mapping(paint_data_mapping, len(self._paint_modes[paint_type]))
|
||||
paint_data_mapping[paint_type] = new_mapping
|
||||
node.callDecoration("setTextureDataMapping", paint_data_mapping)
|
||||
|
||||
|
|
@ -177,12 +171,12 @@ class PaintView(View):
|
|||
return
|
||||
|
||||
if self._current_paint_type == "":
|
||||
self.setPaintType("A")
|
||||
return
|
||||
|
||||
self._paint_shader.setUniformValue("u_bitsRangesStart", self._current_bits_ranges[0])
|
||||
self._paint_shader.setUniformValue("u_bitsRangesEnd", self._current_bits_ranges[1])
|
||||
|
||||
colors = [paint_type_obj.display_color for paint_type_obj in self._paint_modes[self._current_paint_type].types.values()]
|
||||
colors = [paint_type_obj.display_color for paint_type_obj in self._paint_modes[self._current_paint_type].values()]
|
||||
colors_values = [[int(color_part * 255) for color_part in [color.r, color.g, color.b]] for color in colors]
|
||||
self._paint_shader.setUniformValueArray("u_renderColors", colors_values)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue