From 4bede1b555ae4c624c747404ac8ac1e096355e7a Mon Sep 17 00:00:00 2001 From: Nig3l Date: Wed, 6 Aug 2025 09:44:24 +0000 Subject: [PATCH] Image Mask Editor: Expose tools in the Toolbar Expose existing mask operators as tools in the toolbar. The primitive tools are commented out since interactively placement isn't currently supported by the operators. Ref !136086 --- .../keyconfig/keymap_data/blender_default.py | 181 +++++++++++++++++ .../startup/bl_ui/space_toolsystem_toolbar.py | 189 ++++++++++++++++++ source/blender/editors/include/ED_image.hh | 1 + source/blender/editors/include/ED_mask.hh | 3 + source/blender/editors/include/ED_uvedit.hh | 2 +- source/blender/editors/mask/mask_query.cc | 19 ++ .../blender/editors/space_image/image_edit.cc | 9 + .../editors/space_image/space_image.cc | 5 + .../editors/transform/transform_gizmo_2d.cc | 44 +++- source/blender/editors/uvedit/uvedit_ops.cc | 2 +- .../windowmanager/intern/wm_toolsystem.cc | 4 +- 11 files changed, 447 insertions(+), 12 deletions(-) diff --git a/scripts/presets/keyconfig/keymap_data/blender_default.py b/scripts/presets/keyconfig/keymap_data/blender_default.py index 9d8a89dc55b..a1786e9fabf 100644 --- a/scripts/presets/keyconfig/keymap_data/blender_default.py +++ b/scripts/presets/keyconfig/keymap_data/blender_default.py @@ -4791,6 +4791,34 @@ def _template_uv_select(*, type, value, select_passthrough, legacy): return items +def _template_mask_select(*, type, value, select_passthrough, legacy): + + # See: `use_tweak_select_passthrough` doc-string. + if select_passthrough and (value in {'CLICK', 'RELEASE'}): + select_passthrough = False + + items = [ + ("mask.select", {"type": type, "value": value}, + {"properties": [ + *((("deselect_all", True),) if not legacy else ()), + *((("select_passthrough", True),) if select_passthrough else ()), + ]}), + ("mask.select", {"type": type, "value": value, "shift": True}, + {"properties": [("toggle", True)]}), + ] + + if select_passthrough: + # Add an additional click item to de-select all other items, + # needed so pass-through is able to de-select other items. + items.append(( + "mask.select", + {"type": type, "value": 'CLICK'}, + {"properties": [("deselect_all", True)]}, + )) + + return items + + def _template_sequencer_generic_select(*, type, value, legacy): return [( "sequencer.select", @@ -6946,6 +6974,148 @@ def km_image_editor_tool_uv_scale(params): ) +# ------------------------------------------------------------------------------ +# Tool System (Mask Editor) + +def km_image_editor_tool_mask_cursor(params): + return ( + "Image Editor Tool: Mask, Cursor", + {"space_type": 'IMAGE_EDITOR', "region_type": 'WINDOW'}, + {"items": [ + ("mask.cursor_set", {"type": params.tool_mouse, "value": 'PRESS'}, None), + # Don't use `tool_maybe_tweak_event` since it conflicts with `PRESS` that places the cursor. + ("transform.translate", params.tool_tweak_event, + {"properties": [("release_confirm", True), ("cursor_transform", True)]}), + ]}, + ) + + +def km_image_editor_tool_mask_select(params, *, fallback): + return ( + _fallback_id("Image Editor Tool: Mask, Tweak", fallback), + {"space_type": 'IMAGE_EDITOR', "region_type": 'WINDOW'}, + {"items": [ + *([] if (fallback and (params.select_mouse == 'RIGHTMOUSE')) else _template_items_tool_select( + params, "mask.select", "mask.cursor_set", fallback=fallback)), + *([] if params.use_fallback_tool_select_handled else + _template_mask_select( + type=params.select_mouse, + value=params.select_mouse_value, + select_passthrough=params.use_tweak_select_passthrough, + legacy=params.legacy, + )), + ]}, + ) + + +def km_image_editor_tool_mask_select_box(params, *, fallback): + return ( + _fallback_id("Image Editor Tool: Mask, Select Box", fallback), + {"space_type": 'IMAGE_EDITOR', "region_type": 'WINDOW'}, + {"items": [ + *([] if (fallback and not params.use_fallback_tool) else _template_items_tool_select_actions_simple( + "mask.select_box", + # Don't use `tool_maybe_tweak_event`, see comment for this slot. + **(params.select_tweak_event if (fallback and params.use_fallback_tool_select_mouse) else + params.tool_tweak_event))), + ]}, + ) + + +def km_image_editor_tool_mask_select_circle(params, *, fallback): + return ( + _fallback_id("Image Editor Tool: Mask, Select Circle", fallback), + {"space_type": 'IMAGE_EDITOR', "region_type": 'WINDOW'}, + {"items": [ + *([] if (fallback and not params.use_fallback_tool) else _template_items_tool_select_actions_simple( + "mask.select_circle", + **(params.select_tweak_event if (fallback and params.use_fallback_tool_select_mouse) else + {"type": params.tool_mouse, "value": 'PRESS'}), + properties=[("wait_for_input", False)])), + ]}, + ) + + +def km_image_editor_tool_mask_select_lasso(params, *, fallback): + return ( + _fallback_id("Image Editor Tool: Mask, Select Lasso", fallback), + {"space_type": 'IMAGE_EDITOR', "region_type": 'WINDOW'}, + + {"items": [ + *([] if (fallback and not params.use_fallback_tool) else _template_items_tool_select_actions_simple( + "mask.select_lasso", + **(params.select_tweak_event if (fallback and params.use_fallback_tool_select_mouse) else + params.tool_tweak_event))), + ]}, + ) + + +def km_image_editor_tool_mask_move(params): + return ( + "Image Editor Tool: Mask, Move", + {"space_type": 'IMAGE_EDITOR', "region_type": 'WINDOW'}, + {"items": [ + ("transform.translate", {**params.tool_maybe_tweak_event, **params.tool_modifier}, + {"properties": [("release_confirm", True)]}), + ]}, + ) + + +def km_image_editor_tool_mask_rotate(params): + return ( + "Image Editor Tool: Mask, Rotate", + {"space_type": 'IMAGE_EDITOR', "region_type": 'WINDOW'}, + {"items": [ + ("transform.rotate", {**params.tool_maybe_tweak_event, **params.tool_modifier}, + {"properties": [("release_confirm", True)]}), + ]}, + ) + + +def km_image_editor_tool_mask_scale(params): + return ( + "Image Editor Tool: Mask, Scale", + {"space_type": 'IMAGE_EDITOR', "region_type": 'WINDOW'}, + {"items": [ + ("transform.resize", {**params.tool_maybe_tweak_event, **params.tool_modifier}, + {"properties": [("release_confirm", True)]}), + ]}, + ) + + +def km_image_editor_tool_mask_transform(params): + return ( + "Image Editor Tool: Mask, Transform", + {"space_type": 'IMAGE_EDITOR', "region_type": 'WINDOW'}, + {"items": [ + ("transform.resize", {**params.tool_maybe_tweak_event, **params.tool_modifier}, + {"properties": [("release_confirm", True)]}), + ]}, + ) + + +def km_image_editor_tool_mask_primitive_square(params): + return ( + "Image Editor Tool: Mask, Box", + {"space_type": 'IMAGE_EDITOR', "region_type": 'WINDOW'}, + {"items": [ + ("mask.primitive_square_add", {"type": 'LEFTMOUSE', "value": 'PRESS'}, + {"properties": []}), + ]}, + ) + + +def km_image_editor_tool_mask_primitive_circle(params): + return ( + "Image Editor Tool: Mask, Circle", + {"space_type": 'IMAGE_EDITOR', "region_type": 'WINDOW'}, + {"items": [ + ("mask.primitive_circle_add", {"type": 'LEFTMOUSE', "value": 'PRESS'}, + {"properties": []}), + ]}, + ) + + # ------------------------------------------------------------------------------ # Tool System (Node Editor) @@ -8506,6 +8676,17 @@ def generate_keymaps(params=None): km_image_editor_tool_uv_move(params), km_image_editor_tool_uv_rotate(params), km_image_editor_tool_uv_scale(params), + km_image_editor_tool_mask_cursor(params), + *(km_image_editor_tool_mask_select(params, fallback=fallback) for fallback in (False, True)), + *(km_image_editor_tool_mask_select_box(params, fallback=fallback) for fallback in (False, True)), + *(km_image_editor_tool_mask_select_circle(params, fallback=fallback) for fallback in (False, True)), + *(km_image_editor_tool_mask_select_lasso(params, fallback=fallback) for fallback in (False, True)), + km_image_editor_tool_mask_move(params), + km_image_editor_tool_mask_rotate(params), + km_image_editor_tool_mask_scale(params), + km_image_editor_tool_mask_transform(params), + km_image_editor_tool_mask_primitive_circle(params), + km_image_editor_tool_mask_primitive_square(params), *(km_node_editor_tool_select(params, fallback=fallback) for fallback in (False, True)), *(km_node_editor_tool_select_box(params, fallback=fallback) for fallback in (False, True)), *(km_node_editor_tool_select_lasso(params, fallback=fallback) for fallback in (False, True)), diff --git a/scripts/startup/bl_ui/space_toolsystem_toolbar.py b/scripts/startup/bl_ui/space_toolsystem_toolbar.py index fc454b82b34..dbc0153426f 100644 --- a/scripts/startup/bl_ui/space_toolsystem_toolbar.py +++ b/scripts/startup/bl_ui/space_toolsystem_toolbar.py @@ -2467,6 +2467,164 @@ class _defs_image_generic: ) +class _defs_image_mask_transform: + + @ToolDef.from_fn + def translate(): + return dict( + idname="builtin.move", + label="Move", + icon="ops.transform.translate", + widget="IMAGE_GGT_gizmo2d_translate", + operator="transform.translate", + keymap="Image Editor Tool: Mask, Move" + ) + + @ToolDef.from_fn + def rotate(): + return dict( + idname="builtin.rotate", + label="Rotate", + icon="ops.transform.rotate", + widget="IMAGE_GGT_gizmo2d_rotate", + operator="transform.rotate", + keymap="Image Editor Tool: Mask, Rotate", + ) + + @ToolDef.from_fn + def scale(): + return dict( + idname="builtin.scale", + label="Scale", + icon="ops.transform.resize", + widget="IMAGE_GGT_gizmo2d_resize", + operator="transform.resize", + keymap="Image Editor Tool: Mask, Scale", + ) + + @ToolDef.from_fn + def transform(): + return dict( + idname="builtin.transform", + label="Transform", + description=( + "Supports any combination of grab, rotate, and scale at once" + ), + icon="ops.transform.transform", + widget="IMAGE_GGT_gizmo2d", + # No keymap default action, only for gizmo! + ) + + +class _defs_image_mask_select: + + @ToolDef.from_fn + def select(): + return dict( + idname="builtin.select", + label="Tweak", + icon="ops.generic.select", + widget=None, + keymap=(), + ) + + @ToolDef.from_fn + def box(): + def draw_settings(_context, layout, tool): + props = tool.operator_properties("mask.select_box") + row = layout.row() + row.use_property_split = False + row.prop(props, "mode", text="", expand=True, icon_only=True) + + return dict( + idname="builtin.select_box", + label="Select Box", + icon="ops.generic.select_box", + widget=None, + keymap=(), + draw_settings=draw_settings, + ) + + @ToolDef.from_fn + def lasso(): + def draw_settings(_context, layout, tool): + props = tool.operator_properties("mask.select_lasso") + row = layout.row() + row.use_property_split = False + row.prop(props, "mode", text="", expand=True, icon_only=True) + + return dict( + idname="builtin.select_lasso", + label="Select Lasso", + icon="ops.generic.select_lasso", + widget=None, + keymap=(), + draw_settings=draw_settings, + ) + + @ToolDef.from_fn + def circle(): + def draw_settings(_context, layout, tool): + props = tool.operator_properties("mask.select_circle") + row = layout.row() + row.use_property_split = False + row.prop(props, "mode", text="", expand=True, icon_only=True) + layout.prop(props, "radius") + + def draw_cursor(_context, tool, xy): + from gpu_extras.presets import draw_circle_2d + props = tool.operator_properties("mask.select_circle") + radius = props.radius + draw_circle_2d(xy, (1.0,) * 4, radius, segments=32) + + return dict( + idname="builtin.select_circle", + label="Select Circle", + icon="ops.generic.select_circle", + widget=None, + keymap=(), + draw_settings=draw_settings, + draw_cursor=draw_cursor, + ) + + +class _defs_image_mask_primitive: + + @ToolDef.from_fn + def box(): + def draw_settings(_context, layout, tool): + props = tool.operator_properties("mask.primitive_square_add") + layout.prop(props, "size") + layout.prop(props, "location") + + return dict( + idname="builtin.box", + label="Box", + icon="ops.gpencil.primitive_box", + cursor='CROSSHAIR', + draw_settings=draw_settings, + widget=None, + keymap="Image Editor Tool: Mask, Box", + ) + + @ToolDef.from_fn + def circle(): + def draw_settings(_context, layout, tool): + props = tool.operator_properties("mask.primitive_circle_add") + layout.prop(props, "size") + layout.prop(props, "location") + + return dict( + idname="builtin.circle", + label="Circle", + icon="ops.gpencil.primitive_circle", + cursor='CROSSHAIR', + draw_settings=draw_settings, + widget=None, + keymap="Image Editor Tool: Mask, Circle", + ) + + class _defs_image_uv_transform: @ToolDef.from_fn @@ -3121,6 +3279,29 @@ class IMAGE_PT_tools_active(ToolSelectPanelHelper, Panel): ), ) + _tools_mask_transform = ( + _defs_image_mask_transform.translate, + _defs_image_mask_transform.rotate, + _defs_image_mask_transform.scale, + _defs_image_mask_transform.transform, + ) + + _tools_mask_select = ( + ( + _defs_image_mask_select.select, + _defs_image_mask_select.box, + _defs_image_mask_select.circle, + _defs_image_mask_select.lasso, + ), + ) + + _tools_mask_primitive = ( + ( + _defs_image_mask_primitive.circle, + _defs_image_mask_primitive.box, + ), + ) + _tools_annotate = ( ( _defs_annotate.scribble, @@ -3156,7 +3337,15 @@ class IMAGE_PT_tools_active(ToolSelectPanelHelper, Panel): _defs_image_uv_sculpt.pinch, ], 'MASK': [ + *_tools_mask_select, + _defs_image_generic.cursor, None, + *_tools_mask_transform, + None, + *_tools_annotate, + None, + # TODO: Make interactive placement before adding primitive tools + # *_tools_mask_primitive, ], 'PAINT': [ _brush_tool, diff --git a/source/blender/editors/include/ED_image.hh b/source/blender/editors/include/ED_image.hh index b97fa3a7de2..1f37315a6cb 100644 --- a/source/blender/editors/include/ED_image.hh +++ b/source/blender/editors/include/ED_image.hh @@ -112,6 +112,7 @@ bool ED_image_slot_cycle(Image *image, int direction); bool ED_space_image_show_render(const SpaceImage *sima); bool ED_space_image_show_paint(const SpaceImage *sima); +bool ED_space_image_show_mask(const SpaceImage *sima); bool ED_space_image_show_uvedit(const SpaceImage *sima, Object *obedit); bool ED_space_image_paint_curve(const bContext *C); diff --git a/source/blender/editors/include/ED_mask.hh b/source/blender/editors/include/ED_mask.hh index c1b33df3e26..5763fc388be 100644 --- a/source/blender/editors/include/ED_mask.hh +++ b/source/blender/editors/include/ED_mask.hh @@ -90,6 +90,9 @@ bool ED_mask_selected_minmax(const bContext *C, float max[2], bool handles_as_control_point); +void ED_mask_center_from_pivot_ex( + const bContext *C, ScrArea *area, float r_center[2], char mode, bool *r_has_select); + /* `mask_draw.cc` */ /** diff --git a/source/blender/editors/include/ED_uvedit.hh b/source/blender/editors/include/ED_uvedit.hh index 2fcd011de73..8e225aba179 100644 --- a/source/blender/editors/include/ED_uvedit.hh +++ b/source/blender/editors/include/ED_uvedit.hh @@ -64,7 +64,7 @@ bool ED_uvedit_center_multi(const Scene *scene, float r_cent[2], char mode); -bool ED_uvedit_center_from_pivot_ex(SpaceImage *sima, +bool ED_uvedit_center_from_pivot_ex(const SpaceImage *sima, Scene *scene, ViewLayer *view_layer, float r_center[2], diff --git a/source/blender/editors/mask/mask_query.cc b/source/blender/editors/mask/mask_query.cc index 30415967875..7e7daff4555 100644 --- a/source/blender/editors/mask/mask_query.cc +++ b/source/blender/editors/mask/mask_query.cc @@ -673,6 +673,25 @@ bool ED_mask_selected_minmax(const bContext *C, return ok; } +void ED_mask_center_from_pivot_ex( + const bContext *C, ScrArea *area, float r_center[2], char mode, bool *r_has_select) +{ + float min[2], max[2]; + const bool mask_selected = ED_mask_selected_minmax(C, min, max, false); + + switch (mode) { + case V3D_AROUND_CURSOR: + ED_mask_cursor_location_get(area, r_center); + break; + default: + mid_v2_v2v2(r_center, min, max); + break; + } + if (r_has_select != nullptr) { + *r_has_select = mask_selected; + } +} + /** \} */ /* -------------------------------------------------------------------- */ diff --git a/source/blender/editors/space_image/image_edit.cc b/source/blender/editors/space_image/image_edit.cc index 8038e7176de..12e3a77479c 100644 --- a/source/blender/editors/space_image/image_edit.cc +++ b/source/blender/editors/space_image/image_edit.cc @@ -458,6 +458,15 @@ bool ED_space_image_show_paint(const SpaceImage *sima) return (sima->mode == SI_MODE_PAINT); } +bool ED_space_image_show_mask(const SpaceImage *sima) +{ + if (ED_space_image_show_render(sima)) { + return false; + } + + return (sima->mode == SI_MODE_MASK); +} + bool ED_space_image_show_uvedit(const SpaceImage *sima, Object *obedit) { if (sima) { diff --git a/source/blender/editors/space_image/space_image.cc b/source/blender/editors/space_image/space_image.cc index 094f4d72159..c0563f02189 100644 --- a/source/blender/editors/space_image/space_image.cc +++ b/source/blender/editors/space_image/space_image.cc @@ -805,6 +805,11 @@ static void image_main_region_listener(const wmRegionListenerParams *params) } WM_gizmomap_tag_refresh(region->runtime->gizmo_map); break; + case NC_MASK: + if (ELEM(wmn->data, ND_DATA, ND_SELECT)) { + WM_gizmomap_tag_refresh(region->runtime->gizmo_map); + } + break; case NC_MATERIAL: if (wmn->data == ND_SHADING_LINKS) { SpaceImage *sima = static_cast(area->spacedata.first); diff --git a/source/blender/editors/transform/transform_gizmo_2d.cc b/source/blender/editors/transform/transform_gizmo_2d.cc index 4b1dc56cbd2..3686cfbeafc 100644 --- a/source/blender/editors/transform/transform_gizmo_2d.cc +++ b/source/blender/editors/transform/transform_gizmo_2d.cc @@ -39,6 +39,7 @@ #include "ED_gizmo_library.hh" #include "ED_gizmo_utils.hh" #include "ED_image.hh" +#include "ED_mask.hh" #include "ED_screen.hh" #include "ED_uvedit.hh" @@ -81,7 +82,7 @@ static bool gizmo2d_generic_poll(const bContext *C, wmGizmoGroupType *gzgt) case SPACE_IMAGE: { const SpaceImage *sima = static_cast(area->spacedata.first); Object *obedit = CTX_data_edit_object(C); - if (!ED_space_image_show_uvedit(sima, obedit)) { + if (!(ED_space_image_show_uvedit(sima, obedit) || ED_space_image_show_mask(sima))) { return false; } break; @@ -240,12 +241,27 @@ static bool gizmo2d_calc_bounds(const bContext *C, float *r_center, float *r_min ScrArea *area = CTX_wm_area(C); bool has_select = false; if (area->spacetype == SPACE_IMAGE) { - Scene *scene = CTX_data_scene(C); - ViewLayer *view_layer = CTX_data_view_layer(C); - Vector objects = BKE_view_layer_array_from_objects_in_edit_mode_unique_data_with_uvs( - scene, view_layer, nullptr); - if (ED_uvedit_minmax_multi(scene, objects, r_min, r_max)) { - has_select = true; + const SpaceImage *sima = static_cast(area->spacedata.first); + switch (sima->mode) { + case SI_MODE_UV: { + Scene *scene = CTX_data_scene(C); + ViewLayer *view_layer = CTX_data_view_layer(C); + Vector objects = + BKE_view_layer_array_from_objects_in_edit_mode_unique_data_with_uvs( + scene, view_layer, nullptr); + if (ED_uvedit_minmax_multi(scene, objects, r_min, r_max)) { + has_select = true; + } + break; + } + case SI_MODE_MASK: { + if (ED_mask_selected_minmax(C, r_min, r_max, false)) { + has_select = true; + } + break; + } + default: + break; } } else if (area->spacetype == SPACE_SEQ) { @@ -373,9 +389,19 @@ static bool gizmo2d_calc_transform_pivot(const bContext *C, float r_pivot[2]) bool has_select = false; if (area->spacetype == SPACE_IMAGE) { - SpaceImage *sima = static_cast(area->spacedata.first); + const SpaceImage *sima = static_cast(area->spacedata.first); ViewLayer *view_layer = CTX_data_view_layer(C); - ED_uvedit_center_from_pivot_ex(sima, scene, view_layer, r_pivot, sima->around, &has_select); + switch (sima->mode) { + case SI_MODE_UV: + ED_uvedit_center_from_pivot_ex( + sima, scene, view_layer, r_pivot, sima->around, &has_select); + break; + case SI_MODE_MASK: + ED_mask_center_from_pivot_ex(C, area, r_pivot, sima->around, &has_select); + break; + default: + break; + } } else if (area->spacetype == SPACE_SEQ) { SpaceSeq *sseq = static_cast(area->spacedata.first); diff --git a/source/blender/editors/uvedit/uvedit_ops.cc b/source/blender/editors/uvedit/uvedit_ops.cc index 21ca815fe87..6c5b5aa3899 100644 --- a/source/blender/editors/uvedit/uvedit_ops.cc +++ b/source/blender/editors/uvedit/uvedit_ops.cc @@ -309,7 +309,7 @@ bool ED_uvedit_center_multi(const Scene *scene, return changed; } -bool ED_uvedit_center_from_pivot_ex(SpaceImage *sima, +bool ED_uvedit_center_from_pivot_ex(const SpaceImage *sima, Scene *scene, ViewLayer *view_layer, float r_center[2], diff --git a/source/blender/windowmanager/intern/wm_toolsystem.cc b/source/blender/windowmanager/intern/wm_toolsystem.cc index b3908907066..7bb73f367be 100644 --- a/source/blender/windowmanager/intern/wm_toolsystem.cc +++ b/source/blender/windowmanager/intern/wm_toolsystem.cc @@ -728,7 +728,7 @@ static bool toolsystem_key_ensure_check(const bToolKey *tkey) case SPACE_VIEW3D: return true; case SPACE_IMAGE: - if (ELEM(tkey->mode, SI_MODE_PAINT, SI_MODE_UV, SI_MODE_VIEW)) { + if (ELEM(tkey->mode, SI_MODE_PAINT, SI_MODE_UV, SI_MODE_VIEW, SI_MODE_MASK)) { return true; } break; @@ -1140,6 +1140,8 @@ static const char *toolsystem_default_tool(const bToolKey *tkey) return "builtin.brush"; case SI_MODE_VIEW: return "builtin.sample"; + case SI_MODE_MASK: + return "builtin.select_box"; } break; case SPACE_NODE: {