diff --git a/scripts/startup/bl_ui/properties_paint_common.py b/scripts/startup/bl_ui/properties_paint_common.py index 077fa936cfa..db033038835 100644 --- a/scripts/startup/bl_ui/properties_paint_common.py +++ b/scripts/startup/bl_ui/properties_paint_common.py @@ -147,19 +147,50 @@ class BrushSelectPanel(BrushPanel): brush = settings.brush row = layout.row() - large_preview = True - if large_preview: - row.column().template_ID_preview(settings, "brush", new="brush.add", rows=3, cols=8, hide_buttons=False) - else: - row.column().template_ID(settings, "brush", new="brush.add") + # TODO: hide buttons since they are confusing with menu entries. + # But some of this functionality may still be needed. + row.column().template_ID_preview(settings, "brush", new="brush.add", rows=3, cols=8, hide_buttons=True) + + if brush is None: + return + col = row.column() col.menu("VIEW3D_MT_brush_context_menu", icon='DOWNARROW_HLT', text="") - if brush is not None: - col.prop(brush, "use_custom_icon", toggle=True, icon='FILE_IMAGE', text="") + header, panel = layout.panel("customize", default_closed=True) + header.label(text="Customize") + if panel: + panel.use_property_split = True + panel.use_property_decorate = False - if brush.use_custom_icon: - layout.prop(brush, "icon_filepath", text="") + # icon + col = panel.column(heading="Custom Icon", align=True) + row = col.row() + row.prop(brush, "use_custom_icon", text="") + sub = row.row() + sub.active = brush.use_custom_icon + sub.prop(brush, "icon_filepath", text="") + + # brush tool + if context.image_paint_object: + panel.prop(brush, "image_tool") + elif context.vertex_paint_object: + panel.prop(brush, "vertex_tool") + elif context.weight_paint_object: + panel.prop(brush, "weight_tool") + elif context.sculpt_object: + panel.prop(brush, "sculpt_tool") + elif context.tool_settings.curves_sculpt: + panel.prop(brush, "curves_sculpt_tool") + + # brush paint modes + col = panel.column(heading="Modes", align=True) + col.prop(brush, "use_paint_sculpt", text="Sculpt") + col.prop(brush, "use_paint_uv_sculpt", text="UV Sculpt") + col.prop(brush, "use_paint_vertex", text="Vertex Paint") + col.prop(brush, "use_paint_weight", text="Weight Paint") + col.prop(brush, "use_paint_image", text="Texture Paint") + col.prop(brush, "use_paint_sculpt_curves", text="Sculpt Curves") class ColorPalettePanel(BrushPanel): diff --git a/scripts/startup/bl_ui/space_view3d.py b/scripts/startup/bl_ui/space_view3d.py index 3fa8ea2c48f..8a72834a5ec 100644 --- a/scripts/startup/bl_ui/space_view3d.py +++ b/scripts/startup/bl_ui/space_view3d.py @@ -3331,23 +3331,6 @@ class VIEW3D_MT_make_links(Menu): layout.operator("object.datalayout_transfer") -class VIEW3D_MT_brush_paint_modes(Menu): - bl_label = "Enabled Modes" - - def draw(self, context): - layout = self.layout - - settings = UnifiedPaintPanel.paint_settings(context) - brush = settings.brush - - layout.prop(brush, "use_paint_sculpt", text="Sculpt") - layout.prop(brush, "use_paint_uv_sculpt", text="UV Sculpt") - layout.prop(brush, "use_paint_vertex", text="Vertex Paint") - layout.prop(brush, "use_paint_weight", text="Weight Paint") - layout.prop(brush, "use_paint_image", text="Texture Paint") - layout.prop(brush, "use_paint_sculpt_curves", text="Sculpt Curves") - - class VIEW3D_MT_paint_vertex(Menu): bl_label = "Paint" @@ -8900,7 +8883,6 @@ classes = ( VIEW3D_MT_object_cleanup, VIEW3D_MT_make_single_user, VIEW3D_MT_make_links, - VIEW3D_MT_brush_paint_modes, VIEW3D_MT_paint_vertex, VIEW3D_MT_hook, VIEW3D_MT_vertex_group, diff --git a/scripts/startup/bl_ui/space_view3d_toolbar.py b/scripts/startup/bl_ui/space_view3d_toolbar.py index e25bb7d60cb..a42d1cb6b85 100644 --- a/scripts/startup/bl_ui/space_view3d_toolbar.py +++ b/scripts/startup/bl_ui/space_view3d_toolbar.py @@ -43,25 +43,30 @@ class VIEW3D_MT_brush_context_menu(Menu): # skip if no active brush if not brush: - layout.label(text="No Brushes currently available", icon='INFO') + layout.label(text="No brush selected", icon='INFO') return - # brush paint modes - layout.menu("VIEW3D_MT_brush_paint_modes") - # brush tool + # TODO: Need actual check if this is an asset from library. + # TODO: why is brush.asset_data None for these? + is_linked = brush.library + is_override = brush.override_library and brush.override_library.reference + is_asset_brush = is_linked or is_override - if context.image_paint_object: - layout.prop_menu_enum(brush, "image_tool") - elif context.vertex_paint_object: - layout.prop_menu_enum(brush, "vertex_tool") - elif context.weight_paint_object: - layout.prop_menu_enum(brush, "weight_tool") - elif context.sculpt_object: - layout.prop_menu_enum(brush, "sculpt_tool") - layout.operator("brush.reset") - elif context.tool_settings.curves_sculpt: - layout.prop_menu_enum(brush, "curves_sculpt_tool") + if is_asset_brush: + layout.operator("brush.asset_save_as", text="Duplicate Asset...", icon='DUPLICATE') + layout.operator("brush.asset_delete", text="Delete Asset") + + layout.separator() + + layout.operator("brush.asset_update", text="Update Asset") + layout.operator("brush.asset_revert", text="Revert to Asset") + + if context.sculpt_object: + layout.operator("brush.reset", text="Reset to Defaults") + else: + layout.operator("brush.asset_save_as", text="Save As Asset...", icon='FILE_TICK') + layout.operator("brush.asset_delete", text="Delete") class VIEW3D_MT_brush_gpencil_context_menu(Menu): diff --git a/source/blender/asset_system/AS_asset_library.hh b/source/blender/asset_system/AS_asset_library.hh index 513fe29d94b..0150af112f2 100644 --- a/source/blender/asset_system/AS_asset_library.hh +++ b/source/blender/asset_system/AS_asset_library.hh @@ -107,6 +107,9 @@ class AssetLibrary { */ static void foreach_loaded(FunctionRef fn, bool include_all_library); + static std::string resolve_asset_weak_reference_to_full_path( + const AssetWeakReference &asset_reference); + void load_catalogs(); /** Load catalogs that have changed on disk. */ @@ -166,8 +169,6 @@ class AssetLibrary { */ AssetIdentifier asset_identifier_from_library(StringRef relative_asset_path); - std::string resolve_asset_weak_reference_to_full_path(const AssetWeakReference &asset_reference); - eAssetLibraryType library_type() const; StringRefNull name() const; StringRefNull root_path() const; diff --git a/source/blender/blenkernel/BKE_paint.hh b/source/blender/blenkernel/BKE_paint.hh index a0842d85966..ea2a9f12daf 100644 --- a/source/blender/blenkernel/BKE_paint.hh +++ b/source/blender/blenkernel/BKE_paint.hh @@ -8,6 +8,8 @@ * \ingroup bke */ +#include + #include "BLI_array.hh" #include "BLI_bit_vector.hh" #include "BLI_math_matrix_types.hh" @@ -203,6 +205,13 @@ Brush *BKE_paint_brush(Paint *paint); const Brush *BKE_paint_brush_for_read(const Paint *p); void BKE_paint_brush_set(Paint *paint, Brush *br); +/** + * Check if the given brush is a valid Brush Asset. + * + * A valid brush Asset is either an actual asset, or a local liboverride of a linked brush asset. + */ +bool BKE_paint_brush_is_valid_asset(const Brush *brush); + /** * Set the active brush of given paint struct, and store the weak asset reference to it. * \note Takes ownership of the given `weak_asset_reference`. @@ -211,6 +220,12 @@ bool BKE_paint_brush_asset_set(Paint *paint, Brush *brush, AssetWeakReference *weak_asset_reference); +/** + * Get the active brush of given paint struct, together with its weak asset reference. + * \note Returns unset optional if the active brush is not a valid Brush Asset data.. + */ +std::optional BKE_paint_brush_asset_get(Paint *paint, Brush **r_brush); + /** * Attempt to restore a valid active brush in `paint` from brush asset information stored in * `paint`. diff --git a/source/blender/blenkernel/intern/paint.cc b/source/blender/blenkernel/intern/paint.cc index 387af177da6..2226f51bf97 100644 --- a/source/blender/blenkernel/intern/paint.cc +++ b/source/blender/blenkernel/intern/paint.cc @@ -8,6 +8,7 @@ #include #include +#include #include "MEM_guardedalloc.h" @@ -673,6 +674,13 @@ void BKE_paint_brush_set(Paint *p, Brush *br) } } +bool BKE_paint_brush_is_valid_asset(const Brush *brush) +{ + return brush && (ID_IS_ASSET(&brush->id) || + (!ID_IS_LINKED(&brush->id) && ID_IS_OVERRIDE_LIBRARY_REAL(&brush->id) && + ID_IS_ASSET(brush->id.override_library->reference))); +} + static void paint_brush_asset_update(Paint &paint, Brush *brush, AssetWeakReference *brush_asset_reference) @@ -707,6 +715,21 @@ bool BKE_paint_brush_asset_set(Paint *paint, return true; } +std::optional BKE_paint_brush_asset_get(Paint *paint, Brush **r_brush) +{ + Brush *brush = *r_brush = BKE_paint_brush(paint); + + if (!BKE_paint_brush_is_valid_asset(brush)) { + return {}; + } + + if (paint->brush_asset_reference) { + return paint->brush_asset_reference; + } + + return {}; +} + void BKE_paint_brush_asset_restore(Main *bmain, Paint *paint) { if (paint->brush != nullptr) { diff --git a/source/blender/editors/asset/ED_asset_list.hh b/source/blender/editors/asset/ED_asset_list.hh index 0cf919ad419..c57261b754f 100644 --- a/source/blender/editors/asset/ED_asset_list.hh +++ b/source/blender/editors/asset/ED_asset_list.hh @@ -61,7 +61,7 @@ void iterate(const AssetLibraryReference &library_reference, AssetListIterFn fn) void storage_fetch(const AssetLibraryReference *library_reference, const bContext *C); bool is_loaded(const AssetLibraryReference *library_reference); void ensure_previews_job(const AssetLibraryReference *library_reference, const bContext *C); -void clear(const AssetLibraryReference *library_reference, bContext *C); +void clear(const AssetLibraryReference *library_reference, const bContext *C); bool storage_has_list_for_library(const AssetLibraryReference *library_reference); /** * Tag all asset lists in the storage that show main data as needing an update (re-fetch). diff --git a/source/blender/editors/asset/intern/asset_list.cc b/source/blender/editors/asset/intern/asset_list.cc index e5bc5030799..2a346e60811 100644 --- a/source/blender/editors/asset/intern/asset_list.cc +++ b/source/blender/editors/asset/intern/asset_list.cc @@ -116,7 +116,7 @@ class AssetList : NonCopyable { void setup(); void fetch(const bContext &C); void ensurePreviewsJob(const bContext *C); - void clear(bContext *C); + void clear(const bContext *C); AssetHandle asset_get_by_index(int index) const; @@ -257,7 +257,7 @@ void AssetList::ensurePreviewsJob(const bContext *C) } } -void AssetList::clear(bContext *C) +void AssetList::clear(const bContext *C) { /* Based on #ED_fileselect_clear() */ @@ -265,6 +265,7 @@ void AssetList::clear(bContext *C) filelist_readjob_stop(files, CTX_wm_manager(C)); filelist_freelib(files); filelist_clear(files); + filelist_tag_force_reset(files); WM_main_add_notifier(NC_ASSET | ND_ASSET_LIST, nullptr); } @@ -477,7 +478,7 @@ void ensure_previews_job(const AssetLibraryReference *library_reference, const b } } -void clear(const AssetLibraryReference *library_reference, bContext *C) +void clear(const AssetLibraryReference *library_reference, const bContext *C) { AssetList *list = AssetListStorage::lookup_list(*library_reference); if (list) { diff --git a/source/blender/editors/interface/interface_templates.cc b/source/blender/editors/interface/interface_templates.cc index 5c6ebc5d0fc..483d773553b 100644 --- a/source/blender/editors/interface/interface_templates.cc +++ b/source/blender/editors/interface/interface_templates.cc @@ -1374,70 +1374,72 @@ static void template_ID(const bContext *C, template_id_workspace_pin_extra_icon(template_ui, but); - if (ID_IS_LINKED(id)) { - const bool disabled = !BKE_idtype_idcode_is_localizable(GS(id->name)); - if (id->tag & LIB_TAG_INDIRECT) { - but = uiDefIconBut(block, - UI_BTYPE_BUT, - 0, - ICON_LIBRARY_DATA_INDIRECT, - 0, - 0, - UI_UNIT_X, - UI_UNIT_Y, - nullptr, - 0, - 0, - 0, - 0, - TIP_("Indirect library data-block, cannot be made local, " - "Shift + Click to create a library override hierarchy")); + if (!hide_buttons) { + if (ID_IS_LINKED(id)) { + const bool disabled = !BKE_idtype_idcode_is_localizable(GS(id->name)); + if (id->tag & LIB_TAG_INDIRECT) { + but = uiDefIconBut(block, + UI_BTYPE_BUT, + 0, + ICON_LIBRARY_DATA_INDIRECT, + 0, + 0, + UI_UNIT_X, + UI_UNIT_Y, + nullptr, + 0, + 0, + 0, + 0, + TIP_("Indirect library data-block, cannot be made local, " + "Shift + Click to create a library override hierarchy")); + } + else { + but = uiDefIconBut(block, + UI_BTYPE_BUT, + 0, + ICON_LIBRARY_DATA_DIRECT, + 0, + 0, + UI_UNIT_X, + UI_UNIT_Y, + nullptr, + 0, + 0, + 0, + 0, + TIP_("Direct linked library data-block, click to make local, " + "Shift + Click to create a library override")); + } + if (disabled) { + UI_but_flag_enable(but, UI_BUT_DISABLED); + } + else { + UI_but_funcN_set( + but, template_id_cb, MEM_dupallocN(template_ui), POINTER_FROM_INT(UI_ID_LOCAL)); + } } - else { - but = uiDefIconBut(block, - UI_BTYPE_BUT, - 0, - ICON_LIBRARY_DATA_DIRECT, - 0, - 0, - UI_UNIT_X, - UI_UNIT_Y, - nullptr, - 0, - 0, - 0, - 0, - TIP_("Direct linked library data-block, click to make local, " - "Shift + Click to create a library override")); - } - if (disabled) { - UI_but_flag_enable(but, UI_BUT_DISABLED); - } - else { + else if (ID_IS_OVERRIDE_LIBRARY(id)) { + but = uiDefIconBut( + block, + UI_BTYPE_BUT, + 0, + ICON_LIBRARY_DATA_OVERRIDE, + 0, + 0, + UI_UNIT_X, + UI_UNIT_Y, + nullptr, + 0, + 0, + 0, + 0, + TIP_("Library override of linked data-block, click to make fully local, " + "Shift + Click to clear the library override and toggle if it can be edited")); UI_but_funcN_set( - but, template_id_cb, MEM_dupallocN(template_ui), POINTER_FROM_INT(UI_ID_LOCAL)); + but, template_id_cb, MEM_dupallocN(template_ui), POINTER_FROM_INT(UI_ID_OVERRIDE)); } } - else if (ID_IS_OVERRIDE_LIBRARY(id)) { - but = uiDefIconBut( - block, - UI_BTYPE_BUT, - 0, - ICON_LIBRARY_DATA_OVERRIDE, - 0, - 0, - UI_UNIT_X, - UI_UNIT_Y, - nullptr, - 0, - 0, - 0, - 0, - TIP_("Library override of linked data-block, click to make fully local, " - "Shift + Click to clear the library override and toggle if it can be edited")); - UI_but_funcN_set( - but, template_id_cb, MEM_dupallocN(template_ui), POINTER_FROM_INT(UI_ID_OVERRIDE)); - } if ((ID_REAL_USERS(id) > 1) && (hide_buttons == false)) { char numstr[32]; diff --git a/source/blender/editors/sculpt_paint/paint_ops.cc b/source/blender/editors/sculpt_paint/paint_ops.cc index 63f14d1ec13..26eb54e5cf7 100644 --- a/source/blender/editors/sculpt_paint/paint_ops.cc +++ b/source/blender/editors/sculpt_paint/paint_ops.cc @@ -12,8 +12,10 @@ #include "MEM_guardedalloc.h" +#include "BLI_fileops.h" #include "BLI_listbase.h" #include "BLI_math_vector.h" +#include "BLI_path_util.h" #include "BLI_string.h" #include "BLI_utildefines.h" @@ -26,14 +28,23 @@ #include "DNA_object_types.h" #include "DNA_scene_types.h" +#include "BLO_writefile.hh" + +#include "BKE_asset.hh" +#include "BKE_blendfile.hh" #include "BKE_brush.hh" #include "BKE_context.hh" #include "BKE_image.h" #include "BKE_lib_id.hh" +#include "BKE_lib_override.hh" #include "BKE_main.hh" #include "BKE_paint.hh" +#include "BKE_preferences.h" #include "BKE_report.h" +#include "ED_asset_handle.hh" +#include "ED_asset_list.hh" +#include "ED_asset_mark_clear.hh" #include "ED_image.hh" #include "ED_paint.hh" #include "ED_screen.hh" @@ -45,6 +56,7 @@ #include "RNA_access.hh" #include "RNA_define.hh" +#include "AS_asset_library.hh" #include "AS_asset_representation.hh" #include "curves_sculpt_intern.hh" @@ -975,9 +987,13 @@ static void PAINT_OT_brush_select(wmOperatorType *ot) /**************************** Brush Assets **********************************/ -static bool brush_asset_poll(bContext *C) +static bool brush_asset_select_poll(bContext *C) { - return BKE_paint_get_active_from_context(C) != nullptr; + if (BKE_paint_get_active_from_context(C) == nullptr) { + return false; + } + + return CTX_wm_asset(C) != nullptr; } static int brush_asset_select_exec(bContext *C, wmOperator *op) @@ -986,9 +1002,6 @@ static int brush_asset_select_exec(bContext *C, wmOperator *op) * used for the asset-view template. Once the asset list design is used by the Asset Browser, * this can be simplified to just that case. */ blender::asset_system::AssetRepresentation *asset = CTX_wm_asset(C); - if (!asset) { - return OPERATOR_CANCELLED; - } AssetWeakReference *brush_asset_reference = asset->make_weak_reference(); Brush *brush = BKE_brush_asset_runtime_ensure(CTX_data_main(C), brush_asset_reference); @@ -1014,7 +1027,461 @@ static void BRUSH_OT_asset_select(wmOperatorType *ot) ot->idname = "BRUSH_OT_asset_select"; ot->exec = brush_asset_select_exec; - ot->poll = brush_asset_poll; + ot->poll = brush_asset_select_poll; +} + +/* FIXME Quick dirty hack to generate a weak ref from 'raw' paths. + * This needs to be properly implemented in assetlib code. + */ +static AssetWeakReference *brush_asset_create_weakref_hack(const bUserAssetLibrary *user_asset_lib, + std::string &file_path) +{ + AssetWeakReference *asset_weak_ref = MEM_new(__func__); + + blender::StringRefNull asset_root_path = user_asset_lib->dirpath; + BLI_assert(file_path.find(asset_root_path) == 0); + std::string relative_asset_path = file_path.substr(size_t(asset_root_path.size()) + 1); + + asset_weak_ref->asset_library_type = ASSET_LIBRARY_CUSTOM; + asset_weak_ref->asset_library_identifier = BLI_strdup(user_asset_lib->name); + asset_weak_ref->relative_asset_identifier = BLI_strdup(relative_asset_path.c_str()); + + return asset_weak_ref; +} + +static const bUserAssetLibrary *brush_asset_get_editable_library() +{ + /* TODO: take into account which one is marked as default. */ + LISTBASE_FOREACH (const bUserAssetLibrary *, asset_library, &U.asset_libraries) { + return asset_library; + } + return nullptr; +} + +static void brush_asset_refresh_editable_library(const bContext *C) +{ + const bUserAssetLibrary *user_library = brush_asset_get_editable_library(); + + /* TODO: Should the all library reference be automatically cleared? */ + AssetLibraryReference all_lib_ref = blender::asset_system::all_library_reference(); + blender::ed::asset::list::clear(&all_lib_ref, C); + + /* TODO: this is convoluted, can we create a reference from pointer? */ + for (const AssetLibraryReference &lib_ref : + blender::asset_system::all_valid_asset_library_refs()) + { + if (lib_ref.type == ASSET_LIBRARY_CUSTOM) { + const bUserAssetLibrary *ref_user_library = BKE_preferences_asset_library_find_index( + &U, lib_ref.custom_library_index); + if (ref_user_library == user_library) { + blender::ed::asset::list::clear(&lib_ref, C); + return; + } + } + } +} + +static std::string brush_asset_root_path_for_save() +{ + const bUserAssetLibrary *user_library = brush_asset_get_editable_library(); + if (user_library == nullptr || user_library->dirpath[0] == '\0') { + return ""; + } + + char libpath[FILE_MAX]; + BLI_strncpy(libpath, user_library->dirpath, sizeof(libpath)); + BLI_path_slash_native(libpath); + BLI_path_normalize(libpath); + + return std::string(libpath) + SEP + "Saved" + SEP + "Brushes"; +} + +static std::string brush_asset_blendfile_path_for_save(ReportList *reports, + const blender::StringRefNull &base_name) +{ + std::string root_path = brush_asset_root_path_for_save(); + BLI_assert(!root_path.empty()); + + if (!BLI_dir_create_recursive(root_path.c_str())) { + BKE_report(reports, RPT_ERROR, "Failed to create asset library directory to save brush"); + return ""; + } + + char base_name_filesafe[FILE_MAXFILE]; + BLI_strncpy(base_name_filesafe, base_name.c_str(), sizeof(base_name_filesafe)); + BLI_path_make_safe_filename(base_name_filesafe); + + if (!BLI_is_file((root_path + SEP + base_name_filesafe + BLENDER_ASSET_FILE_SUFFIX).c_str())) { + return root_path + SEP + base_name_filesafe + BLENDER_ASSET_FILE_SUFFIX; + } + int i = 1; + while (BLI_is_file((root_path + SEP + base_name_filesafe + "_" + std::to_string(i++) + + BLENDER_ASSET_FILE_SUFFIX) + .c_str())) + ; + return root_path + SEP + base_name_filesafe + "_" + std::to_string(i - 1) + + BLENDER_ASSET_FILE_SUFFIX; +} + +static bool brush_asset_write_in_library(Main *bmain, + Brush *brush, + const char *name, + const blender::StringRefNull &filepath, + std::string &final_full_file_path, + ReportList *reports) +{ + /* XXX + * FIXME + * + * This code is _pure evil_. It does in-place manipulation on IDs in global Main database, + * temporarilly remove them and add them back... + * + * Use it as-is for now (in a similar way as python API or copy-to-buffer works). Nut the whole + * 'BKE_blendfile_write_partial' code needs to be completely refactored. + * + * Ideas: + * - Have `BKE_blendfile_write_partial_begin` return a new temp Main. + * - Replace `BKE_blendfile_write_partial_tag_ID` by API to add IDs to this temp Main. + * + This should _duplicate_ the ID, not remove the original one from the source Main! + * - Have API to automatically also duplicate dependencies into temp Main. + * + Have options to e.g. make all duplicated IDs 'local' (i.e. remove their library data). + * - `BKE_blendfile_write_partial` then simply write the given temp main. + * - `BKE_blendfile_write_partial_end` frees the temp Main. + */ + + const short brush_flag = brush->id.flag; + const int brush_tag = brush->id.tag; + const int brush_us = brush->id.us; + const std::string brush_name = brush->id.name + 2; + IDOverrideLibrary *brush_liboverride = brush->id.override_library; + AssetMetaData *brush_asset_data = brush->id.asset_data; + const int write_flags = 0; /* Could use #G_FILE_COMPRESS ? */ + const eBLO_WritePathRemap remap_mode = BLO_WRITE_PATH_REMAP_RELATIVE; + + BKE_blendfile_write_partial_begin(bmain); + + brush->id.flag |= LIB_FAKEUSER; + brush->id.tag &= ~LIB_TAG_RUNTIME; + brush->id.us = 1; + BLI_strncpy(brush->id.name + 2, name, sizeof(brush->id.name) - 2); + if (!ID_IS_ASSET(&brush->id)) { + brush->id.asset_data = brush->id.override_library->reference->asset_data; + } + brush->id.override_library = nullptr; + + BKE_blendfile_write_partial_tag_ID(&brush->id, true); + + /* TODO: check overwriting existing file. */ + /* TODO: ensure filepath contains only valid characters for file system. */ + const bool sucess = BKE_blendfile_write_partial( + bmain, filepath.c_str(), write_flags, remap_mode, reports); + + if (sucess) { + final_full_file_path = std::string(filepath) + SEP + "Brush" + SEP + name; + } + + BKE_blendfile_write_partial_end(bmain); + + BKE_blendfile_write_partial_tag_ID(&brush->id, false); + brush->id.flag = brush_flag; + brush->id.tag = brush_tag; + brush->id.us = brush_us; + BLI_strncpy(brush->id.name + 2, brush_name.c_str(), sizeof(brush->id.name) - 2); + brush->id.override_library = brush_liboverride; + brush->id.asset_data = brush_asset_data; + + return sucess; +} + +static bool brush_asset_save_as_poll(bContext *C) +{ + Paint *paint = BKE_paint_get_active_from_context(C); + Brush *brush = (paint) ? BKE_paint_brush(paint) : nullptr; + if (paint == nullptr || brush == nullptr) { + return false; + } + + const bUserAssetLibrary *user_library = brush_asset_get_editable_library(); + if (user_library == nullptr || user_library->dirpath[0] == '\0') { + CTX_wm_operator_poll_msg_set(C, "No default asset library available to save to"); + return false; + } + + return true; +} + +static int brush_asset_save_as_exec(bContext *C, wmOperator *op) +{ + Paint *paint = BKE_paint_get_active_from_context(C); + Brush *brush = (paint) ? BKE_paint_brush(paint) : nullptr; + if (paint == nullptr || brush == nullptr) { + return OPERATOR_CANCELLED; + } + + /* Determine file path to save to. */ + PropertyRNA *name_prop = RNA_struct_find_property(op->ptr, "name"); + char name[MAX_NAME] = ""; + if (RNA_property_is_set(op->ptr, name_prop)) { + RNA_property_string_get(op->ptr, name_prop, name); + } + if (name[0] == '\0') { + STRNCPY(name, brush->id.name + 2); + } + + const std::string filepath = brush_asset_blendfile_path_for_save(op->reports, name); + if (filepath.empty()) { + return OPERATOR_CANCELLED; + } + + /* Turn brush into asset if it isn't yet. */ + if (!BKE_paint_brush_is_valid_asset(brush)) { + blender::ed::asset::mark_id(&brush->id); + blender::ed::asset::generate_preview(C, &brush->id); + } + BLI_assert(BKE_paint_brush_is_valid_asset(brush)); + + /* Save to asset library. */ + std::string final_full_asset_filepath; + const bool sucess = brush_asset_write_in_library( + CTX_data_main(C), brush, name, filepath, final_full_asset_filepath, op->reports); + + if (!sucess) { + BKE_report(op->reports, RPT_ERROR, "Failed to write to asset library"); + return OPERATOR_CANCELLED; + } + + /* Create weak reference to new datablock. */ + const bUserAssetLibrary *asset_lib = brush_asset_get_editable_library(); + AssetWeakReference *new_brush_weak_ref = brush_asset_create_weakref_hack( + asset_lib, final_full_asset_filepath); + + /* TODO: maybe not needed, even less so if there is more visual confirmation of change. */ + BKE_reportf(op->reports, RPT_INFO, "Saved \"%s\"", filepath.c_str()); + + Main *bmain = CTX_data_main(C); + brush = BKE_brush_asset_runtime_ensure(bmain, new_brush_weak_ref); + + if (!BKE_paint_brush_asset_set(paint, brush, new_brush_weak_ref)) { + /* Note brush sset 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"); + } + + brush_asset_refresh_editable_library(C); + WM_main_add_notifier(NC_ASSET | ND_ASSET_LIST | NA_ADDED, nullptr); + + return OPERATOR_FINISHED; +} + +static int brush_asset_save_as_invoke(bContext *C, wmOperator *op, const wmEvent * /*event*/) +{ + Paint *paint = BKE_paint_get_active_from_context(C); + Brush *brush = BKE_paint_brush(paint); + + RNA_string_set(op->ptr, "name", brush->id.name + 2); + + /* TODO: add information about the asset library this will be saved to? */ + /* TODO: autofocus name? */ + return WM_operator_props_dialog_popup(C, op, 400); +} + +static void BRUSH_OT_asset_save_as(wmOperatorType *ot) +{ + ot->name = "Save As Brush Asset"; + ot->description = + "Save a copy of the active brush asset into the default asset library, and make it the " + "active brush"; + ot->idname = "BRUSH_OT_asset_save_as"; + + ot->exec = brush_asset_save_as_exec; + ot->invoke = brush_asset_save_as_invoke; + ot->poll = brush_asset_save_as_poll; + + RNA_def_string(ot->srna, "name", nullptr, MAX_NAME, "Name", "Name used to save the brush asset"); +} + +static bool brush_asset_is_editable(const AssetWeakReference &brush_weak_ref) +{ + /* Fairly simple checks, based on filepath only: + * - The blendlib filepath ends up with the `.asset.blend` extension. + * - The blendlib is located in the expected sub-directory of the editable asset library. + * + * TODO: Right now no check is done on file content, e.g. to ensure that the blendlib file has + * not been manually edited by the user (that it does not have any UI IDs e.g.). */ + + char path_buffer[FILE_MAX_LIBEXTRA]; + char *dir, *group, *name; + AS_asset_full_path_explode_from_weak_ref(&brush_weak_ref, path_buffer, &dir, &group, &name); + + if (!blender::StringRef(dir).endswith(BLENDER_ASSET_FILE_SUFFIX)) { + return false; + } + + std::string root_path_for_save = brush_asset_root_path_for_save(); + if (root_path_for_save.empty() || !blender::StringRef(dir).startswith(root_path_for_save)) { + return false; + } + + /* TODO: Do we want more checks here? E.g. check actual content of the file? */ + return true; +} + +static bool brush_asset_delete_poll(bContext *C) +{ + Paint *paint = BKE_paint_get_active_from_context(C); + Brush *brush = (paint) ? BKE_paint_brush(paint) : nullptr; + if (paint == nullptr || brush == nullptr) { + return false; + } + + /* Asset brush, check if belongs to an editable blend file. */ + if (paint->brush_asset_reference && BKE_paint_brush_is_valid_asset(brush)) { + if (!brush_asset_is_editable(*paint->brush_asset_reference)) { + CTX_wm_operator_poll_msg_set(C, "Asset blend file is not editable"); + return false; + } + } + + return true; +} + +static int brush_asset_delete_exec(bContext *C, wmOperator *op) +{ + Main *bmain = CTX_data_main(C); + Paint *paint = BKE_paint_get_active_from_context(C); + Brush *brush = BKE_paint_brush(paint); + + if (paint->brush_asset_reference && BKE_paint_brush_is_valid_asset(brush)) { + /* Delete from asset library on disk. */ + char path_buffer[FILE_MAX_LIBEXTRA]; + char *filepath; + AS_asset_full_path_explode_from_weak_ref( + paint->brush_asset_reference, path_buffer, &filepath, nullptr, nullptr); + + if (BLI_delete(filepath, false, false) != 0) { + BKE_report(op->reports, RPT_ERROR, "Failed to delete asset library file"); + } + } + + /* Delete from session. If local override, also delete linked one. + * TODO: delete both in one step? */ + ID *original_brush = (!ID_IS_LINKED(&brush->id) && ID_IS_OVERRIDE_LIBRARY_REAL(&brush->id)) ? + brush->id.override_library->reference : + nullptr; + BKE_id_delete(bmain, brush); + if (original_brush) { + BKE_id_delete(bmain, original_brush); + } + + brush_asset_refresh_editable_library(C); + WM_main_add_notifier(NC_ASSET | ND_ASSET_LIST | NA_REMOVED, nullptr); + + /* TODO: activate default brush. */ + + return OPERATOR_FINISHED; +} + +static void BRUSH_OT_asset_delete(wmOperatorType *ot) +{ + ot->name = "Delete Brush Asset"; + ot->description = "Delete the active brush asset both from the local session and asset library"; + ot->idname = "BRUSH_OT_asset_delete"; + + ot->exec = brush_asset_delete_exec; + ot->invoke = WM_operator_confirm; + ot->poll = brush_asset_delete_poll; +} + +static bool brush_asset_update_poll(bContext *C) +{ + Paint *paint = BKE_paint_get_active_from_context(C); + Brush *brush = (paint) ? BKE_paint_brush(paint) : nullptr; + if (paint == nullptr || brush == nullptr) { + return false; + } + + if (!(paint->brush_asset_reference && BKE_paint_brush_is_valid_asset(brush))) { + return false; + } + + if (!brush_asset_is_editable(*paint->brush_asset_reference)) { + CTX_wm_operator_poll_msg_set(C, "Asset blend file is not editable"); + return false; + } + + return true; +} + +static int brush_asset_update_exec(bContext *C, wmOperator *op) +{ + Paint *paint = BKE_paint_get_active_from_context(C); + Brush *brush = nullptr; + const AssetWeakReference *brush_weak_ref = + BKE_paint_brush_asset_get(paint, &brush).value_or(nullptr); + + char path_buffer[FILE_MAX_LIBEXTRA]; + char *filepath; + AS_asset_full_path_explode_from_weak_ref( + brush_weak_ref, path_buffer, &filepath, nullptr, nullptr); + + BLI_assert(BKE_paint_brush_is_valid_asset(brush)); + + std::string final_full_asset_filepath; + brush_asset_write_in_library(CTX_data_main(C), + brush, + brush->id.name + 2, + filepath, + final_full_asset_filepath, + op->reports); + + return OPERATOR_FINISHED; +} + +static void BRUSH_OT_asset_update(wmOperatorType *ot) +{ + ot->name = "Update Brush Asset"; + ot->description = "Update the active brush asset in the asset library with current settings"; + ot->idname = "BRUSH_OT_asset_update"; + + ot->exec = brush_asset_update_exec; + ot->poll = brush_asset_update_poll; +} + +static bool brush_asset_revert_poll(bContext *C) +{ + /* TODO: check if there is anything to revert? */ + Paint *paint = BKE_paint_get_active_from_context(C); + Brush *brush = (paint) ? BKE_paint_brush(paint) : nullptr; + if (paint == nullptr || brush == nullptr) { + return false; + } + + return paint->brush_asset_reference && BKE_paint_brush_is_valid_asset(brush); +} + +static int brush_asset_revert_exec(bContext *C, wmOperator * /*op*/) +{ + Main *bmain = CTX_data_main(C); + Paint *paint = BKE_paint_get_active_from_context(C); + Brush *brush = BKE_paint_brush(paint); + + /* TODO: check if doing this for the hierarchy is ok. */ + /* TODO: the overrides don't update immediately when tweaking brush settings. */ + BKE_lib_override_library_id_hierarchy_reset(bmain, &brush->id, false); + + WM_main_add_notifier(NC_BRUSH | NA_EDITED, brush); + + return OPERATOR_FINISHED; +} + +static void BRUSH_OT_asset_revert(wmOperatorType *ot) +{ + ot->name = "Revert Brush Asset"; + ot->description = + "Revert the active brush settings to the default values from the asset library"; + ot->idname = "BRUSH_OT_asset_revert"; + + ot->exec = brush_asset_revert_exec; + ot->poll = brush_asset_revert_poll; } /***** Stencil Control *****/ @@ -1479,6 +1946,10 @@ void ED_operatortypes_paint() WM_operatortype_append(BRUSH_OT_stencil_fit_image_aspect); WM_operatortype_append(BRUSH_OT_stencil_reset_transform); WM_operatortype_append(BRUSH_OT_asset_select); + WM_operatortype_append(BRUSH_OT_asset_save_as); + WM_operatortype_append(BRUSH_OT_asset_delete); + WM_operatortype_append(BRUSH_OT_asset_update); + WM_operatortype_append(BRUSH_OT_asset_revert); /* NOTE: particle uses a different system, can be added with existing operators in `wm.py`. */ WM_operatortype_append(PAINT_OT_brush_select);