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:
@@ -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;
|
||||
|
||||
@@ -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 []))
|
||||
|
||||
Reference in New Issue
Block a user