diff --git a/plugins/PaintTool/BrushColorButton.qml b/plugins/PaintTool/BrushColorButton.qml
new file mode 100644
index 0000000000..71556f2681
--- /dev/null
+++ b/plugins/PaintTool/BrushColorButton.qml
@@ -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)
+ }
+}
diff --git a/plugins/PaintTool/BrushShapeButton.qml b/plugins/PaintTool/BrushShapeButton.qml
new file mode 100644
index 0000000000..5c290e4a13
--- /dev/null
+++ b/plugins/PaintTool/BrushShapeButton.qml
@@ -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)
+ }
+}
diff --git a/plugins/PaintTool/PaintModeButton.qml b/plugins/PaintTool/PaintModeButton.qml
new file mode 100644
index 0000000000..473996e04b
--- /dev/null
+++ b/plugins/PaintTool/PaintModeButton.qml
@@ -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)
+ }
+}
diff --git a/plugins/PaintTool/PaintTool.py b/plugins/PaintTool/PaintTool.py
index 0c3ac0d661..524011af9d 100644
--- a/plugins/PaintTool/PaintTool.py
+++ b/plugins/PaintTool/PaintTool.py
@@ -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
diff --git a/plugins/PaintTool/PaintTool.qml b/plugins/PaintTool/PaintTool.qml
index 602805cba1..4cbe9d4ade 100644
--- a/plugins/PaintTool/PaintTool.qml
+++ b/plugins/PaintTool/PaintTool.qml
@@ -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()
+ }
}
diff --git a/plugins/PaintTool/PaintView.py b/plugins/PaintTool/PaintView.py
index 32124872c4..22eb8c55f6 100644
--- a/plugins/PaintTool/PaintView.py
+++ b/plugins/PaintTool/PaintView.py
@@ -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)
diff --git a/resources/qml/PrintSetupSelector/Recommended/RecommendedQualityProfileSelectorButton.qml b/resources/qml/ModeSelectorButton.qml
similarity index 91%
rename from resources/qml/PrintSetupSelector/Recommended/RecommendedQualityProfileSelectorButton.qml
rename to resources/qml/ModeSelectorButton.qml
index 1bbc726b9d..65a6ee4a75 100644
--- a/resources/qml/PrintSetupSelector/Recommended/RecommendedQualityProfileSelectorButton.qml
+++ b/resources/qml/ModeSelectorButton.qml
@@ -17,7 +17,7 @@ Rectangle
color: mouseArea.containsMouse || selected ? UM.Theme.getColor("background_3") : UM.Theme.getColor("background_1")
property bool selected: false
- property string profileName: ""
+ property alias text: mainLabel.text
property string icon: ""
property string custom_icon: ""
property alias tooltipText: tooltip.text
@@ -42,18 +42,18 @@ Rectangle
Item
{
- width: intentIcon.width
+ width: mainIcon.width
anchors
{
top: parent.top
- bottom: qualityLabel.top
+ bottom: mainLabel.top
horizontalCenter: parent.horizontalCenter
topMargin: UM.Theme.getSize("narrow_margin").height
}
Item
{
- id: intentIcon
+ id: mainIcon
width: UM.Theme.getSize("recommended_button_icon").width
height: UM.Theme.getSize("recommended_button_icon").height
@@ -90,7 +90,7 @@ Rectangle
{
id: initialLabel
anchors.centerIn: parent
- text: profileName.charAt(0).toUpperCase()
+ text: base.text.charAt(0).toUpperCase()
font: UM.Theme.getFont("small_bold")
horizontalAlignment: Text.AlignHCenter
}
@@ -102,8 +102,7 @@ Rectangle
UM.Label
{
- id: qualityLabel
- text: profileName
+ id: mainLabel
anchors
{
bottom: parent.bottom
diff --git a/resources/qml/PrintSetupSelector/Recommended/RecommendedQualityProfileSelector.qml b/resources/qml/PrintSetupSelector/Recommended/RecommendedQualityProfileSelector.qml
index 19c57e5130..1559f6cec3 100644
--- a/resources/qml/PrintSetupSelector/Recommended/RecommendedQualityProfileSelector.qml
+++ b/resources/qml/PrintSetupSelector/Recommended/RecommendedQualityProfileSelector.qml
@@ -7,7 +7,6 @@ import QtQuick.Layouts 2.10
import UM 1.5 as UM
import Cura 1.7 as Cura
-import ".."
Item
{
@@ -28,9 +27,9 @@ Item
id: intentSelectionRepeater
model: Cura.IntentSelectionModel {}
- RecommendedQualityProfileSelectorButton
+ Cura.ModeSelectorButton
{
- profileName: model.name
+ text: model.name
icon: model.icon ? model.icon : ""
custom_icon: model.custom_icon ? model.custom_icon : ""
tooltipText: model.description ? model.description : ""
diff --git a/resources/themes/cura-light/icons/default/Circle.svg b/resources/themes/cura-light/icons/default/Circle.svg
new file mode 100644
index 0000000000..c69b5a4e31
--- /dev/null
+++ b/resources/themes/cura-light/icons/default/Circle.svg
@@ -0,0 +1,5 @@
+
+
diff --git a/resources/themes/cura-light/icons/default/Eraser.svg b/resources/themes/cura-light/icons/default/Eraser.svg
new file mode 100644
index 0000000000..fbe5103993
--- /dev/null
+++ b/resources/themes/cura-light/icons/default/Eraser.svg
@@ -0,0 +1,5 @@
+
+
diff --git a/resources/themes/cura-light/icons/default/Seam.svg b/resources/themes/cura-light/icons/default/Seam.svg
new file mode 100644
index 0000000000..a9615832d6
--- /dev/null
+++ b/resources/themes/cura-light/icons/default/Seam.svg
@@ -0,0 +1,6 @@
+
+
diff --git a/resources/themes/cura-light/icons/low/CancelBadge.svg b/resources/themes/cura-light/icons/low/CancelBadge.svg
new file mode 100644
index 0000000000..25c4198083
--- /dev/null
+++ b/resources/themes/cura-light/icons/low/CancelBadge.svg
@@ -0,0 +1,5 @@
+
+
diff --git a/resources/themes/cura-light/icons/low/CheckBadge.svg b/resources/themes/cura-light/icons/low/CheckBadge.svg
new file mode 100644
index 0000000000..a10a92c6af
--- /dev/null
+++ b/resources/themes/cura-light/icons/low/CheckBadge.svg
@@ -0,0 +1,5 @@
+
+