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
This commit is contained in:
Nig3l
2025-08-06 09:44:24 +00:00
committed by Campbell Barton
parent 77bb71d8da
commit 4bede1b555
11 changed files with 447 additions and 12 deletions

View File

@@ -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)),

View File

@@ -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,

View File

@@ -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);

View File

@@ -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` */
/**

View File

@@ -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],

View File

@@ -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;
}
}
/** \} */
/* -------------------------------------------------------------------- */

View File

@@ -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) {

View File

@@ -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<SpaceImage *>(area->spacedata.first);

View File

@@ -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<const SpaceImage *>(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,13 +241,28 @@ 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) {
const SpaceImage *sima = static_cast<const SpaceImage *>(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<Object *> objects = BKE_view_layer_array_from_objects_in_edit_mode_unique_data_with_uvs(
Vector<Object *> 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) {
Scene *scene = CTX_data_scene(C);
@@ -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<SpaceImage *>(area->spacedata.first);
const SpaceImage *sima = static_cast<const SpaceImage *>(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<SpaceSeq *>(area->spacedata.first);

View File

@@ -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],

View File

@@ -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: {