Merge branch 'CURA-12660_painting-UI-improvements' into CURA-12449_handling-painted-models-map

This commit is contained in:
Erwan MATHIEU 2025-08-05 14:08:45 +02:00
commit 9e186af74b
13 changed files with 264 additions and 93 deletions

View file

@ -59,6 +59,7 @@ from cura import ApplicationMetadata
from cura.API import CuraAPI
from cura.API.Account import Account
from cura.Arranging.ArrangeObjectsJob import ArrangeObjectsJob
from cura.CuraRenderer import CuraRenderer
from cura.Machines.MachineErrorChecker import MachineErrorChecker
from cura.Machines.Models.BuildPlateModel import BuildPlateModel
from cura.Machines.Models.CustomQualityProfilesDropDownMenuModel import CustomQualityProfilesDropDownMenuModel
@ -361,6 +362,9 @@ class CuraApplication(QtApplication):
self._machine_action_manager = MachineActionManager(self)
self._machine_action_manager.initialize()
def makeRenderer(self) -> CuraRenderer:
return CuraRenderer(self)
def __sendCommandToSingleInstance(self):
self._single_instance = SingleInstance(self, self._files_to_open, self._urls_to_open)

46
cura/CuraRenderer.py Normal file
View file

@ -0,0 +1,46 @@
# Copyright (c) 2025 UltiMaker
# Uranium is released under the terms of the LGPLv3 or higher.
from typing import TYPE_CHECKING
from cura.PickingPass import PickingPass
from UM.Qt.QtRenderer import QtRenderer
from UM.View.RenderPass import RenderPass
from UM.View.SelectionPass import SelectionPass
if TYPE_CHECKING:
from cura.CuraApplication import CuraApplication
class CuraRenderer(QtRenderer):
"""An overridden Renderer implementation that adds some behaviors specific to Cura."""
def __init__(self, application: "CuraApplication") -> None:
super().__init__()
self._controller = application.getController()
self._controller.activeToolChanged.connect(self._onActiveToolChanged)
self._extra_rendering_passes: list[RenderPass] = []
def _onActiveToolChanged(self) -> None:
tool_extra_rendering_passes = []
active_tool = self._controller.getActiveTool()
if active_tool is not None:
tool_extra_rendering_passes = active_tool.getRequiredExtraRenderingPasses()
for extra_rendering_pass in self._extra_rendering_passes:
extra_rendering_pass.setEnabled(extra_rendering_pass.getName() in tool_extra_rendering_passes)
def _makeRenderPasses(self) -> list[RenderPass]:
self._extra_rendering_passes = [
SelectionPass(self._viewport_width, self._viewport_height, SelectionPass.SelectionMode.FACES),
PickingPass(self._viewport_width, self._viewport_height, only_selected_objects=True),
PickingPass(self._viewport_width, self._viewport_height, only_selected_objects=False)
]
for extra_rendering_pass in self._extra_rendering_passes:
extra_rendering_pass.setEnabled(False)
return super()._makeRenderPasses() + self._extra_rendering_passes

View file

@ -7,6 +7,7 @@ from UM.Qt.QtApplication import QtApplication
from UM.Logger import Logger
from UM.Math.Vector import Vector
from UM.Resources import Resources
from UM.Scene.Selection import Selection
from UM.View.RenderPass import RenderPass
from UM.View.GL.OpenGL import OpenGL
@ -27,13 +28,14 @@ class PickingPass(RenderPass):
.. note:: that in order to increase precision, the 24 bit depth value is encoded into all three of the R,G & B channels
"""
def __init__(self, width: int, height: int) -> None:
super().__init__("picking", width, height)
def __init__(self, width: int, height: int, only_selected_objects: bool = False) -> None:
super().__init__("picking" if not only_selected_objects else "picking_selected", width, height)
self._renderer = QtApplication.getInstance().getRenderer()
self._shader = None #type: Optional[ShaderProgram]
self._scene = QtApplication.getInstance().getController().getScene()
self._only_selected_objects = only_selected_objects
def render(self) -> None:
if not self._shader:
@ -53,7 +55,7 @@ class PickingPass(RenderPass):
# Fill up the batch with objects that can be sliced. `
for node in DepthFirstIterator(self._scene.getRoot()): #type: ignore #Ignore type error because iter() should get called automatically by Python syntax.
if node.callDecoration("isSliceable") and node.getMeshData() and node.isVisible():
if node.callDecoration("isSliceable") and node.getMeshData() and node.isVisible() and (not self._only_selected_objects or Selection.isSelected(node)):
batch.addItem(node.getWorldTransformation(copy = False), node.getMeshData(), normal_transformation=node.getCachedNormalMatrix())
self.bind()

View file

@ -16,7 +16,7 @@ from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
class XRayPass(RenderPass):
def __init__(self, width, height):
super().__init__("xray", width, height)
super().__init__("xray", width, height, -100)
self._shader = None
self._gl = OpenGL.getInstance().getBindingsObject()

View file

@ -13,13 +13,27 @@ UM.ToolbarButton
property string color
checked: base.selectedColor === buttonBrushColor.color
onClicked: setColor()
function setColor()
{
base.selectedColor = buttonBrushColor.color
UM.Controller.triggerActionWithData("setBrushColor", buttonBrushColor.color)
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()
}
}

View file

@ -13,13 +13,27 @@ UM.ToolbarButton
property int shape
checked: base.selectedShape === buttonBrushShape.shape
onClicked: setShape()
function setShape()
{
base.selectedShape = buttonBrushShape.shape
UM.Controller.triggerActionWithData("setBrushShape", buttonBrushShape.shape)
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()
}
}

View file

@ -6,19 +6,34 @@ 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)
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()
}
}

View file

@ -16,8 +16,11 @@ from UM.Logger import Logger
from UM.Scene.SceneNode import SceneNode
from UM.Scene.Selection import Selection
from UM.Tool import Tool
from UM.View.GL.OpenGL import OpenGL
from cura.CuraApplication import CuraApplication
from cura.PickingPass import PickingPass
from UM.View.SelectionPass import SelectionPass
from .PaintView import PaintView
@ -34,6 +37,7 @@ class PaintTool(Tool):
super().__init__()
self._picking_pass: Optional[PickingPass] = None
self._faces_selection_pass: Optional[SelectionPass] = None
self._shortcut_key: Qt.Key = Qt.Key.Key_P
@ -41,9 +45,9 @@ class PaintTool(Tool):
self._mesh_transformed_cache = None
self._cache_dirty: bool = True
self._brush_size: int = 10
self._brush_color: str = ""
self._brush_shape: PaintTool.Brush.Shape = PaintTool.Brush.Shape.SQUARE
self._brush_size: int = 200
self._brush_color: str = "preferred"
self._brush_shape: PaintTool.Brush.Shape = PaintTool.Brush.Shape.CIRCLE
self._brush_pen: QPen = self._createBrushPen()
self._mouse_held: bool = False
@ -52,6 +56,10 @@ class PaintTool(Tool):
self._last_mouse_coords: Optional[Tuple[int, int]] = None
self._last_face_id: Optional[int] = None
self.setExposedProperties("PaintType", "BrushSize", "BrushColor", "BrushShape")
Selection.selectionChanged.connect(self._updateIgnoreUnselectedObjects)
def _createBrushPen(self) -> QPen:
pen = QPen()
pen.setWidth(self._brush_size)
@ -86,28 +94,51 @@ 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()
def setPaintType(self, paint_type: str) -> None:
paint_view = self._get_paint_view()
if paint_view is None:
return
paint_view.setPaintType(paint_type)
if paint_type != self.getPaintType():
paint_view.setPaintType(paint_type)
self._brush_pen = self._createBrushPen()
self._updateScene()
self._brush_pen = self._createBrushPen()
self._updateScene()
self.propertyChanged.emit()
def getBrushSize(self) -> int:
return self._brush_size
def setBrushSize(self, brush_size: float) -> None:
if brush_size != self._brush_size:
self._brush_size = int(brush_size)
brush_size_int = int(brush_size)
if brush_size_int != self._brush_size:
self._brush_size = brush_size_int
self._brush_pen = self._createBrushPen()
self.propertyChanged.emit()
def getBrushColor(self) -> str:
return self._brush_color
def setBrushColor(self, brush_color: str) -> None:
self._brush_color = brush_color
if brush_color != self._brush_color:
self._brush_color = brush_color
self.propertyChanged.emit()
def getBrushShape(self) -> int:
return self._brush_shape
def setBrushShape(self, brush_shape: int) -> None:
if brush_shape != self._brush_shape:
self._brush_shape = brush_shape
self._brush_pen = self._createBrushPen()
self.propertyChanged.emit()
def undoStackAction(self, redo_instead: bool) -> bool:
paint_view = self._get_paint_view()
@ -179,7 +210,7 @@ class PaintTool(Tool):
self._cache_dirty = True
def _getTexCoordsFromClick(self, node: SceneNode, x: float, y: float) -> Tuple[int, Optional[numpy.ndarray]]:
face_id = self._selection_pass.getFaceIdAtPosition(x, y)
face_id = self._faces_selection_pass.getFaceIdAtPosition(x, y)
if face_id < 0 or face_id >= node.getMeshData().getFaceCount():
return face_id, None
@ -234,6 +265,23 @@ class PaintTool(Tool):
self._iteratateSplitSubstroke(node, substrokes, mid_struct, info_b)
self._iteratateSplitSubstroke(node, substrokes, info_a, mid_struct)
def _setupNodeForPainting(self, node: SceneNode) -> bool:
mesh = node.getMeshData()
if mesh.hasUVCoordinates():
return True
texture_width, texture_height = mesh.calculateUnwrappedUVCoordinates()
if texture_width <= 0 or texture_height <= 0:
return False
node.callDecoration("prepareTexture", texture_width, texture_height)
if hasattr(mesh, OpenGL.VertexBufferProperty):
# Force clear OpenGL buffer so that new UV coordinates will be sent
delattr(mesh, OpenGL.VertexBufferProperty)
return True
def event(self, event: Event) -> bool:
"""Handle mouse and keyboard events.
@ -250,13 +298,14 @@ class PaintTool(Tool):
# Make sure the displayed values are updated if the bounding box of the selected mesh(es) changes
if event.type == Event.ToolActivateEvent:
controller.setActiveStage("PrepareStage")
controller.setActiveView("PaintTool") # Because that's the plugin-name, and the view is registered to it.
self._updateIgnoreUnselectedObjects()
return True
if event.type == Event.ToolDeactivateEvent:
controller.setActiveStage("PrepareStage")
controller.setActiveView("SolidView")
CuraApplication.getInstance().getRenderer().getRenderPass("selection").setIgnoreUnselectedObjects(False)
CuraApplication.getInstance().getRenderer().getRenderPass("selection_faces").setIgnoreUnselectedObjects(False)
return True
if event.type == Event.MouseReleaseEvent and self._controller.getToolsEnabled():
@ -285,8 +334,15 @@ class PaintTool(Tool):
if paintview is None:
return False
if not self._selection_pass:
return False
if not self._faces_selection_pass:
self._faces_selection_pass = CuraApplication.getInstance().getRenderer().getRenderPass("selection_faces")
if not self._faces_selection_pass:
return False
if not self._picking_pass:
self._picking_pass = CuraApplication.getInstance().getRenderer().getRenderPass("picking_selected")
if not self._picking_pass:
return False
camera = self._controller.getScene().getActiveCamera()
if not camera:
@ -297,17 +353,15 @@ class PaintTool(Tool):
self._node_cache.transformationChanged.disconnect(self._nodeTransformChanged)
self._node_cache = node
self._node_cache.transformationChanged.connect(self._nodeTransformChanged)
self._cache_dirty = True
if self._cache_dirty:
self._cache_dirty = False
self._mesh_transformed_cache = self._node_cache.getMeshDataTransformed()
if not self._mesh_transformed_cache:
return False
if not self._picking_pass:
self._picking_pass = PickingPass(camera.getViewportWidth(), camera.getViewportHeight())
self._picking_pass.render()
self._selection_pass.renderFacesMode()
if not self._setupNodeForPainting(node):
return False
face_id, texcoords = self._getTexCoordsFromClick(node, mouse_evt.x, mouse_evt.y)
if texcoords is None:
@ -348,4 +402,13 @@ class PaintTool(Tool):
if node is None:
node = Selection.getSelectedObject(0)
if node is not None:
Application.getInstance().getController().getScene().sceneChanged.emit(node)
Application.getInstance().getController().getScene().sceneChanged.emit(node)
def getRequiredExtraRenderingPasses(self) -> list[str]:
return ["selection_faces", "picking_selected"]
def _updateIgnoreUnselectedObjects(self):
if self._controller.getActiveTool() is self:
ignore_unselected_objects = len(Selection.getAllSelectedObjects()) == 1
CuraApplication.getInstance().getRenderer().getRenderPass("selection").setIgnoreUnselectedObjects(ignore_unselected_objects)
CuraApplication.getInstance().getRenderer().getRenderPass("selection_faces").setIgnoreUnselectedObjects(ignore_unselected_objects)

View file

@ -15,10 +15,6 @@ Item
height: childrenRect.height
UM.I18nCatalog { id: catalog; name: "cura"}
property string selectedMode: ""
property string selectedColor: ""
property int selectedShape: 0
Action
{
id: undoAction
@ -57,6 +53,7 @@ Item
icon: "Support"
tooltipText: catalog.i18nc("@tooltip", "Refine support placement by defining preferred/avoidance areas")
mode: "support"
visible: false
}
PaintModeButton
@ -174,19 +171,18 @@ Item
from: 10
to: 1000
value: 200
onPressedChanged: function(pressed)
{
if(! pressed)
{
setBrushSize()
UM.Controller.setProperty("BrushSize", shapeSizeSlider.value);
}
}
function setBrushSize()
Component.onCompleted:
{
UM.Controller.triggerActionWithData("setBrushSize", shapeSizeSlider.value)
shapeSizeSlider.value = UM.Controller.properties.getValue("BrushSize");
}
}
@ -239,13 +235,4 @@ Item
}
}
}
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()
shapeSizeSlider.setBrushSize()
}
}

View file

@ -3,15 +3,17 @@
import os
from PyQt6.QtCore import QRect
from typing import Optional, List, Tuple, Dict
from typing import Optional, List, Tuple, Dict, cast
from PyQt6.QtGui import QImage, QColor, QPainter
from cura.CuraApplication import CuraApplication
from cura.BuildVolume import BuildVolume
from UM.PluginRegistry import PluginRegistry
from UM.View.View import View
from UM.View.GL.ShaderProgram import ShaderProgram
from UM.View.GL.Texture import Texture
from UM.View.View import View
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
from UM.Scene.Selection import Selection
from UM.View.GL.OpenGL import OpenGL
from UM.i18n import i18nCatalog
@ -44,7 +46,11 @@ class PaintView(View):
self._force_opaque_mask = QImage(2, 2, QImage.Format.Format_Mono)
self._force_opaque_mask.fill(1)
CuraApplication.getInstance().engineCreatedSignal.connect(self._makePaintModes)
application = CuraApplication.getInstance()
application.engineCreatedSignal.connect(self._makePaintModes)
self._scene = application.getController().getScene()
self._solid_view = None
def _makePaintModes(self):
theme = CuraApplication.getInstance().getTheme()
@ -57,6 +63,8 @@ class PaintView(View):
"extruder": usual_types,
}
self._current_paint_type = "seam"
def _checkSetup(self):
if not self._paint_shader:
shader_filename = os.path.join(PluginRegistry.getInstance().getPluginPath("PaintTool"), "paint.shader")
@ -77,6 +85,8 @@ class PaintView(View):
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()
bit_range_start, bit_range_end = self._current_bits_ranges
@ -136,30 +146,25 @@ class PaintView(View):
return self._current_paint_texture.getWidth(), self._current_paint_texture.getHeight()
return 0, 0
def getPaintType(self) -> str:
return self._current_paint_type
def setPaintType(self, paint_type: str) -> None:
self._current_paint_type = paint_type
def _prepareDataMapping(self):
node = Selection.getAllSelectedObjects()[0]
if node is None:
return
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]))
paint_data_mapping[paint_type] = new_mapping
if self._current_paint_type not in paint_data_mapping:
new_mapping = self._add_mapping(paint_data_mapping, len(self._paint_modes[self._current_paint_type]))
paint_data_mapping[self._current_paint_type] = new_mapping
node.callDecoration("setTextureDataMapping", paint_data_mapping)
mesh = node.getMeshData()
if not mesh.hasUVCoordinates():
texture_width, texture_height = mesh.calculateUnwrappedUVCoordinates()
if texture_width > 0 and texture_height > 0:
node.callDecoration("prepareTexture", texture_width, texture_height)
if hasattr(mesh, OpenGL.VertexBufferProperty):
# Force clear OpenGL buffer so that new UV coordinates will be sent
delattr(mesh, OpenGL.VertexBufferProperty)
self._current_paint_type = paint_type
self._current_bits_ranges = paint_data_mapping[paint_type]
self._current_bits_ranges = paint_data_mapping[self._current_paint_type]
@staticmethod
def _add_mapping(actual_mapping: Dict[str, tuple[int, int]], nb_storable_values: int) -> tuple[int, int]:
@ -172,17 +177,35 @@ class PaintView(View):
return start_index, end_index
def beginRendering(self) -> None:
renderer = self.getRenderer()
if self._current_paint_type not in self._paint_modes:
return
if self._solid_view is None:
plugin_registry = PluginRegistry.getInstance()
solid_view = plugin_registry.getPluginObject("SolidView")
if isinstance(solid_view, View):
self._solid_view = solid_view
display_objects = Selection.getAllSelectedObjects().copy()
if len(display_objects) != 1 and self._solid_view is not None:
# Display the classic view until a single object is selected
self._solid_view.beginRendering()
return
self._checkSetup()
renderer = self.getRenderer()
for node in DepthFirstIterator(self._scene.getRoot()):
if isinstance(node, BuildVolume):
node.render(renderer)
paint_batch = renderer.createRenderBatch(shader=self._paint_shader)
renderer.addRenderBatch(paint_batch)
node = Selection.getSelectedObject(0)
if node is None:
return
if self._current_paint_type == "":
return
for node in display_objects:
paint_batch.addItem(node.getWorldTransformation(copy=False), node.getMeshData(), normal_transformation=node.getCachedNormalMatrix())
self._current_paint_texture = node.callDecoration("getPaintTexture")
self._paint_shader.setTexture(0, self._current_paint_texture)
self._paint_shader.setUniformValue("u_bitsRangesStart", self._current_bits_ranges[0])
self._paint_shader.setUniformValue("u_bitsRangesEnd", self._current_bits_ranges[1])
@ -190,8 +213,3 @@ class PaintView(View):
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)
self._current_paint_texture = node.callDecoration("getPaintTexture")
self._paint_shader.setTexture(0, self._current_paint_texture)
paint_batch.addItem(node.getWorldTransformation(copy=False), node.getMeshData(), normal_transformation=node.getCachedNormalMatrix())

View file

@ -55,7 +55,7 @@ fragment =
color_index = (color_index << (32 - 1 - u_bitsRangesEnd)) >> 32 - 1 - (u_bitsRangesEnd - u_bitsRangesStart);
vec4 diffuse_color = vec4(u_renderColors[color_index] / 255.0, 1.0);
highp float n_dot_l = clamp(dot(normal, light_dir), 0.0, 1.0);
highp float n_dot_l = mix(0.3, 0.7, dot(normal, light_dir));
final_color += (n_dot_l * diffuse_color);
final_color.a = u_opacity;
@ -122,7 +122,7 @@ fragment41core =
color_index = (color_index << (32 - 1 - u_bitsRangesEnd)) >> 32 - 1 - (u_bitsRangesEnd - u_bitsRangesStart);
vec4 diffuse_color = vec4(u_renderColors[color_index] / 255.0, 1.0);
highp float n_dot_l = clamp(dot(normal, light_dir), 0.0, 1.0);
highp float n_dot_l = mix(0.3, 0.7, dot(normal, light_dir));
final_color += (n_dot_l * diffuse_color);
final_color.a = u_opacity;
@ -132,7 +132,7 @@ fragment41core =
[defaults]
u_ambientColor = [0.3, 0.3, 0.3, 1.0]
u_opacity = 0.5
u_opacity = 1.0
u_texture = 0
[bindings]

View file

@ -289,8 +289,9 @@ class SolidView(View):
def endRendering(self):
# check whether the xray overlay is showing badness
if time.time() > self._next_xray_checking_time\
and Application.getInstance().getPreferences().getValue(self._show_xray_warning_preference):
if (time.time() > self._next_xray_checking_time
and Application.getInstance().getPreferences().getValue(self._show_xray_warning_preference)
and self._xray_pass is not None):
self._next_xray_checking_time = time.time() + self._xray_checking_update_time
xray_img = self._xray_pass.getOutput()

View file

@ -1,6 +1,8 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from typing import Optional
from PyQt6.QtCore import Qt, QTimer
from PyQt6.QtWidgets import QApplication
@ -35,6 +37,7 @@ class SupportEraser(Tool):
self._controller = self.getController()
self._selection_pass = None
self._picking_pass: Optional[PickingPass] = None
CuraApplication.getInstance().globalContainerStackChanged.connect(self._updateEnabled)
# Note: if the selection is cleared with this tool active, there is no way to switch to
@ -84,12 +87,13 @@ class SupportEraser(Tool):
# Only "normal" meshes can have anti_overhang_meshes added to them
return
# Create a pass for picking a world-space location from the mouse location
active_camera = self._controller.getScene().getActiveCamera()
picking_pass = PickingPass(active_camera.getViewportWidth(), active_camera.getViewportHeight())
picking_pass.render()
# Get the pass for picking a world-space location from the mouse location
if self._picking_pass is None:
self._picking_pass = Application.getInstance().getRenderer().getRenderPass("picking_selected")
if not self._picking_pass:
return
picked_position = picking_pass.getPickedPosition(event.x, event.y)
picked_position = self._picking_pass.getPickedPosition(event.x, event.y)
# Add the anti_overhang_mesh cube at the picked location
self._createEraserMesh(picked_node, picked_position)
@ -189,3 +193,6 @@ class SupportEraser(Tool):
mesh.calculateNormals()
return mesh
def getRequiredExtraRenderingPasses(self) -> list[str]:
return ["picking_selected"]