From bbddcab4e9520af04e390dc7c9ffe739488ddef1 Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Wed, 2 Jul 2025 14:04:41 +0200 Subject: [PATCH] Proper paint-on-seam UI CURA-12578 --- plugins/PaintTool/BrushColorButton.qml | 25 ++ plugins/PaintTool/BrushShapeButton.qml | 25 ++ plugins/PaintTool/PaintModeButton.qml | 24 ++ plugins/PaintTool/PaintTool.py | 19 +- plugins/PaintTool/PaintTool.qml | 264 +++++++++--------- plugins/PaintTool/PaintView.py | 38 ++- ...ectorButton.qml => ModeSelectorButton.qml} | 13 +- .../RecommendedQualityProfileSelector.qml | 5 +- .../cura-light/icons/default/Circle.svg | 5 + .../cura-light/icons/default/Eraser.svg | 5 + .../themes/cura-light/icons/default/Seam.svg | 6 + .../cura-light/icons/low/CancelBadge.svg | 5 + .../cura-light/icons/low/CheckBadge.svg | 5 + 13 files changed, 274 insertions(+), 165 deletions(-) create mode 100644 plugins/PaintTool/BrushColorButton.qml create mode 100644 plugins/PaintTool/BrushShapeButton.qml create mode 100644 plugins/PaintTool/PaintModeButton.qml rename resources/qml/{PrintSetupSelector/Recommended/RecommendedQualityProfileSelectorButton.qml => ModeSelectorButton.qml} (91%) create mode 100644 resources/themes/cura-light/icons/default/Circle.svg create mode 100644 resources/themes/cura-light/icons/default/Eraser.svg create mode 100644 resources/themes/cura-light/icons/default/Seam.svg create mode 100644 resources/themes/cura-light/icons/low/CancelBadge.svg create mode 100644 resources/themes/cura-light/icons/low/CheckBadge.svg 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 @@ + + + +