Paint: Add toggle support for brush.asset_activate

This commit adds a toggle functionality to the `brush.asset_activate`
operator that makes it behave similarly to the `paint.brush_select`
parameter of the same name.

When the operator has this option enabled, using the operator or
pressing the relevant key will either:
* Activate the specified brush and store it if the current brush does
  not match the specified brush
* Activate the previously stored brush if it exists.

This option is exposed in the keymaps and enabled by default for the
Sculpt Mask brush.

This allows, for example, users to press 'M' to switch to the mask brush
and then press 'M' again to switch back to their previously active
brush.

Partially addresses this RCS submission: [1]
[1] https://blender.community/c/rightclickselect/1VwZ/

---

### Notes
This commit does not currently clear the `AssetWeakReference` when switching paint modes.

Pull Request: https://projects.blender.org/blender/blender/pulls/138845
This commit is contained in:
Sean Kim
2025-05-16 23:25:45 +02:00
committed by Sean Kim
parent fe0b230a2b
commit 9e1e9b0859
6 changed files with 75 additions and 2 deletions

View File

@@ -5211,6 +5211,7 @@ def km_sculpt(params):
{"properties": [
("asset_library_type", 'ESSENTIALS'),
("relative_asset_identifier", "brushes/essentials_brushes-mesh_sculpt.blend/Brush/Mask"),
("use_toggle", True)
]}),
*_template_asset_shelf_popup("VIEW3D_AST_brush_sculpt", params.spacebar_action),
])

View File

@@ -238,6 +238,9 @@ bool BKE_paint_brush_set(Main *bmain,
const AssetWeakReference *brush_asset_reference);
bool BKE_paint_brush_set_default(Main *bmain, Paint *paint);
bool BKE_paint_brush_set_essentials(Main *bmain, Paint *paint, const char *name);
void BKE_paint_previous_asset_reference_set(Paint *paint,
AssetWeakReference &&asset_weak_reference);
void BKE_paint_previous_asset_reference_clear(Paint *paint);
std::optional<AssetWeakReference> BKE_paint_brush_type_default_reference(
eObjectMode ob_mode, std::optional<int> brush_type);

View File

@@ -1090,6 +1090,20 @@ bool BKE_paint_brush_set_essentials(Main *bmain, Paint *paint, const char *name)
return paint_brush_update_from_asset_reference(bmain, paint);
}
void BKE_paint_previous_asset_reference_set(Paint *paint,
AssetWeakReference &&asset_weak_reference)
{
if (!paint->runtime.previous_active_brush_reference) {
paint->runtime.previous_active_brush_reference = MEM_new<AssetWeakReference>(__func__);
}
*paint->runtime.previous_active_brush_reference = asset_weak_reference;
}
void BKE_paint_previous_asset_reference_clear(Paint *paint)
{
MEM_SAFE_DELETE(paint->runtime.previous_active_brush_reference);
}
void BKE_paint_brushes_validate(Main *bmain, Paint *paint)
{
/* Clear brush with invalid mode. Unclear if this can still happen,
@@ -1786,6 +1800,7 @@ void BKE_paint_free(Paint *paint)
MEM_delete(brush_ref->brush_asset_reference);
MEM_delete(brush_ref);
}
MEM_delete(paint->runtime.previous_active_brush_reference);
}
void BKE_paint_copy(const Paint *src, Paint *dst, const int flag)

View File

@@ -69,12 +69,24 @@ static wmOperatorStatus brush_asset_activate_exec(bContext *C, wmOperator *op)
return OPERATOR_CANCELLED;
}
const bool use_toggle = RNA_boolean_get(op->ptr, "use_toggle");
AssetWeakReference brush_asset_reference = asset->make_weak_reference();
Paint *paint = BKE_paint_get_active_from_context(C);
std::optional<AssetWeakReference> asset_to_save;
if (use_toggle) {
BLI_assert(paint->brush_asset_reference);
if (brush_asset_reference == *paint->brush_asset_reference) {
if (paint->runtime.previous_active_brush_reference != nullptr) {
brush_asset_reference = *paint->runtime.previous_active_brush_reference;
}
}
else {
asset_to_save = *paint->brush_asset_reference;
}
}
Brush *brush = reinterpret_cast<Brush *>(
bke::asset_edit_id_from_weak_reference(*bmain, ID_BR, brush_asset_reference));
Paint *paint = BKE_paint_get_active_from_context(C);
/* Activate brush through tool system rather than calling #BKE_paint_brush_set() directly, to let
* the tool system switch tools if necessary, and update which brush was the last recently used
* one for the current tool. */
@@ -84,6 +96,15 @@ static wmOperatorStatus brush_asset_activate_exec(bContext *C, wmOperator *op)
return OPERATOR_FINISHED;
}
if (asset_to_save) {
BKE_paint_previous_asset_reference_set(paint, std::move(*asset_to_save));
}
else if (!use_toggle) {
/* If we aren't toggling, clear the previous reference so that we don't swap back to an
* incorrect "previous" asset */
BKE_paint_previous_asset_reference_clear(paint);
}
WM_main_add_notifier(NC_ASSET | NA_ACTIVATED, nullptr);
WM_main_add_notifier(NC_SCENE | ND_TOOLSETTINGS, nullptr);
@@ -99,6 +120,13 @@ void BRUSH_OT_asset_activate(wmOperatorType *ot)
ot->exec = brush_asset_activate_exec;
asset::operator_asset_reference_props_register(*ot->srna);
PropertyRNA *prop;
prop = RNA_def_boolean(ot->srna,
"use_toggle",
false,
"Toggle",
"Switch between the current and assigned brushes on consecutive uses.");
RNA_def_property_flag(prop, PropertyFlag(PROP_HIDDEN | PROP_SKIP_SAVE));
}
static bool brush_asset_save_as_poll(bContext *C)

View File

@@ -978,6 +978,8 @@ typedef struct Paint_Runtime {
unsigned int initialized;
unsigned short ob_mode;
char _pad[2];
/** The last brush that was active. Used to support toggling. */
struct AssetWeakReference *previous_active_brush_reference;
} Paint_Runtime;
typedef struct NamedBrushAssetReference {

View File

@@ -12,6 +12,9 @@ class AssetActivateTest(unittest.TestCase):
def setUp(self):
# Test case isn't specific to Sculpt Mode, but we need a paint mode in general.
bpy.ops.object.mode_set(mode='SCULPT')
bpy.ops.brush.asset_activate(
asset_library_type='ESSENTIALS',
relative_asset_identifier='brushes/essentials_brushes-mesh_sculpt.blend/Brush/Draw')
def test_loads_essential_asset(self):
result = bpy.ops.brush.asset_activate(
@@ -19,6 +22,27 @@ class AssetActivateTest(unittest.TestCase):
relative_asset_identifier='brushes/essentials_brushes-mesh_sculpt.blend/Brush/Smooth')
self.assertEqual({'FINISHED'}, result)
def test_toggle_when_brush_differs_sets_specified_brush(self):
"""Test that using the 'Toggle' parameter when the brush is not active still activates the correct brush"""
bpy.ops.brush.asset_activate(
asset_library_type='ESSENTIALS',
relative_asset_identifier='brushes/essentials_brushes-mesh_sculpt.blend/Brush/Mask',
use_toggle=True)
self.assertEqual(bpy.context.tool_settings.sculpt.brush.name, 'Mask')
def test_toggle_when_brush_matches_sets_previous_brush(self):
"""Test that using the 'Toggle' parameter when the brush is active activates the previously activated brush"""
bpy.ops.brush.asset_activate(
asset_library_type='ESSENTIALS',
relative_asset_identifier='brushes/essentials_brushes-mesh_sculpt.blend/Brush/Mask',
use_toggle=True)
self.assertEqual(bpy.context.tool_settings.sculpt.brush.name, 'Mask')
bpy.ops.brush.asset_activate(
asset_library_type='ESSENTIALS',
relative_asset_identifier='brushes/essentials_brushes-mesh_sculpt.blend/Brush/Mask',
use_toggle=True)
self.assertEqual(bpy.context.tool_settings.sculpt.brush.name, 'Draw')
if __name__ == "__main__":
# Drop all arguments before "--", or everything if the delimiter is absent. Keep the executable path.