diff --git a/source/blender/editors/sculpt_paint/brush_asset_ops.cc b/source/blender/editors/sculpt_paint/brush_asset_ops.cc index dced0ffc825..1b7509060e1 100644 --- a/source/blender/editors/sculpt_paint/brush_asset_ops.cc +++ b/source/blender/editors/sculpt_paint/brush_asset_ops.cc @@ -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 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 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( + 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( - 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; diff --git a/tests/python/bl_brush_test.py b/tests/python/bl_brush_test.py index bc30aa1d24b..75f53cfb91c 100644 --- a/tests/python/bl_brush_test.py +++ b/tests/python/bl_brush_test.py @@ -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 []))