Paint: Allow duplicating essential brushes into current file

With the release of the brush assets project in 4.3, most brush
management moved out of the current .blend file into either the
essentials library or user-created libraries.

With this change, a number of workflows became more difficult:
* Handling a large library of texture and texture properties for brushes
  due to ID linkage constraints.
* Having local tweaks to a brush without bloating the the asset library
  and reducing discoverability.

This commit introduces an intermediate step to assist with both of the
preceding pain points. The `brush.asset_save_as` operator is extended
to allow saving into the current blend file via the `Duplicate Asset`
context menu entry.

Additionally, these features help ease authoring brush assets in general
from the UI instead of requiring manual datablock management.

This allows brushes to be stored alongside other data inside a specific
blend file.

Related to #129655 and [1].

[1] https://blender.community/c/rightclickselect/XYMA/

Pull Request: https://projects.blender.org/blender/blender/pulls/138105
This commit is contained in:
Sean Kim
2025-05-20 22:02:42 +02:00
committed by Sean Kim
parent 7ada8e28d7
commit 4ecde8dc53
2 changed files with 76 additions and 25 deletions

View File

@@ -18,6 +18,7 @@
#include "BKE_brush.hh"
#include "BKE_context.hh"
#include "BKE_global.hh"
#include "BKE_lib_id.hh"
#include "BKE_paint.hh"
#include "BKE_preferences.h"
#include "BKE_preview_image.hh"
@@ -142,11 +143,6 @@ static bool brush_asset_save_as_poll(bContext *C)
return false;
}
if (BLI_listbase_is_empty(&U.asset_libraries)) {
CTX_wm_operator_poll_msg_set(C, "No asset library available to save to");
return false;
}
return true;
}
@@ -166,18 +162,30 @@ static wmOperatorStatus brush_asset_save_as_exec(bContext *C, wmOperator *op)
STRNCPY(name, brush->id.name + 2);
}
const bUserAssetLibrary *user_library = asset::get_asset_library_from_opptr(*op->ptr);
if (!user_library) {
return OPERATOR_CANCELLED;
}
const eAssetLibraryType enum_value = (eAssetLibraryType)RNA_enum_get(op->ptr,
"asset_library_reference");
const bool is_local_library = enum_value == ASSET_LIBRARY_LOCAL;
asset_system::AssetLibrary *library = AS_asset_library_load(
bmain, asset::user_library_to_library_ref(*user_library));
AssetLibraryReference library_reference;
const bUserAssetLibrary *user_library = nullptr;
if (is_local_library) {
library_reference = asset_system::current_file_library_reference();
}
else {
user_library = asset::get_asset_library_from_opptr(*op->ptr);
if (!user_library) {
return OPERATOR_CANCELLED;
}
library_reference = asset::user_library_to_library_ref(*user_library);
}
asset_system::AssetLibrary *library = AS_asset_library_load(bmain, library_reference);
if (!library) {
BKE_report(op->reports, RPT_ERROR, "Failed to load asset library");
return OPERATOR_CANCELLED;
}
BLI_assert(is_local_library || user_library);
/* Turn brush into asset if it isn't yet. */
if (!ID_IS_ASSET(&brush->id)) {
asset::mark_id(&brush->id);
@@ -185,7 +193,23 @@ static wmOperatorStatus brush_asset_save_as_exec(bContext *C, wmOperator *op)
}
BLI_assert(ID_IS_ASSET(&brush->id));
if (is_local_library) {
const Brush *original_brush = brush;
brush = BKE_brush_duplicate(
bmain, brush, USER_DUP_OBDATA | USER_DUP_LINKED_ID, LIB_ID_DUPLICATE_IS_ROOT_ID);
BKE_libblock_rename(*bmain, brush->id, name);
asset::mark_id(&brush->id);
BLI_assert(brush->id.us == 1);
BKE_asset_metadata_free(&brush->id.asset_data);
brush->id.asset_data = BKE_asset_metadata_copy(original_brush->id.asset_data);
BLI_assert(brush->id.asset_data != nullptr);
}
/* Add asset to catalog. */
/* Note: This needs to happen after the local asset is created but BEFORE a non-local library
* is saved */
char catalog_path_c[MAX_NAME];
RNA_string_get(op->ptr, "catalog_path", catalog_path_c);
@@ -197,28 +221,36 @@ static wmOperatorStatus brush_asset_save_as_exec(bContext *C, wmOperator *op)
BKE_asset_metadata_catalog_id_set(&meta_data, catalog.catalog_id, catalog.simple_name.c_str());
}
AssetWeakReference brush_asset_reference;
const std::optional<std::string> final_full_asset_filepath = bke::asset_edit_id_save_as(
*bmain, brush->id, name, *user_library, brush_asset_reference, *op->reports);
if (!final_full_asset_filepath) {
return OPERATOR_CANCELLED;
if (!is_local_library) {
AssetWeakReference brush_asset_reference;
const std::optional<std::string> final_full_asset_filepath = bke::asset_edit_id_save_as(
*bmain, brush->id, name, *user_library, brush_asset_reference, *op->reports);
if (!final_full_asset_filepath) {
return OPERATOR_CANCELLED;
}
library->catalog_service().write_to_disk(*final_full_asset_filepath);
brush = reinterpret_cast<Brush *>(
bke::asset_edit_id_from_weak_reference(*bmain, ID_BR, brush_asset_reference));
brush->has_unsaved_changes = false;
}
library->catalog_service().write_to_disk(*final_full_asset_filepath);
asset::shelf::show_catalog_in_visible_shelves(*C, catalog_path_c);
brush = reinterpret_cast<Brush *>(
bke::asset_edit_id_from_weak_reference(*bmain, ID_BR, brush_asset_reference));
brush->has_unsaved_changes = false;
if (!WM_toolsystem_activate_brush_and_tool(C, paint, brush)) {
/* Note brush asset was still saved in editable asset library, so was not a no-op. */
BKE_report(op->reports, RPT_WARNING, "Unable to activate just-saved brush asset");
}
asset::refresh_asset_library(C, *user_library);
asset::refresh_asset_library(C, library_reference);
WM_main_add_notifier(NC_ASSET | ND_ASSET_LIST | NA_ADDED, nullptr);
WM_main_add_notifier(NC_BRUSH | NA_EDITED, brush);
if (is_local_library) {
WM_main_add_notifier(NC_BRUSH | NA_ADDED, brush);
WM_file_tag_modified();
}
else {
WM_main_add_notifier(NC_BRUSH | NA_EDITED, brush);
}
return OPERATOR_FINISHED;
}
@@ -287,8 +319,7 @@ static const EnumPropertyItem *rna_asset_library_reference_itemf(bContext * /*C*
const EnumPropertyItem *items = asset::library_reference_to_rna_enum_itemf(
/* Only get writable libraries. */
/*include_readonly=*/false,
/* Saving brushes to the current file isn't working correctly yet. */
/*include_current_file=*/false);
/*include_current_file=*/true);
if (!items) {
*r_free = false;
return nullptr;

View File

@@ -44,6 +44,26 @@ class AssetActivateTest(unittest.TestCase):
self.assertEqual(bpy.context.tool_settings.sculpt.brush.name, 'Draw')
class AssetSaveAsTest(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/Smooth')
def test_saves_asset_locally(self):
"""Test that saving an asset to the local creates a copy correctly"""
result = bpy.ops.brush.asset_save_as(name="Local Copy", asset_library_reference="LOCAL", catalog_path="")
self.assertEqual({'FINISHED'}, result)
self.assertTrue("Local Copy" in bpy.data.brushes)
local_brush = bpy.data.brushes["Local Copy"]
self.assertEqual(local_brush.sculpt_tool, 'SMOOTH')
self.assertGreaterEqual(local_brush.users, 1)
if __name__ == "__main__":
# Drop all arguments before "--", or everything if the delimiter is absent. Keep the executable path.
unittest.main(argv=sys.argv[:1] + (sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else []))