Brush Assets: Save As, Delete, Revert and Update operators #117513

Merged
Brecht Van Lommel merged 10 commits from mont29/blender:bap-saveas-asset into brush-assets-project 2024-01-28 00:01:17 +01:00
10 changed files with 645 additions and 114 deletions

View File

@ -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):

View File

@ -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,

View File

@ -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):

View File

@ -107,6 +107,9 @@ class AssetLibrary {
*/
static void foreach_loaded(FunctionRef<void(AssetLibrary &)> 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;

View File

@ -8,6 +8,8 @@
* \ingroup bke
*/
#include <optional>
#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<AssetWeakReference *> 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`.

View File

@ -8,6 +8,7 @@
#include <cstdlib>
#include <cstring>
#include <optional>
#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<AssetWeakReference *> 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) {

View File

@ -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).

View File

@ -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) {

View File

@ -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];

View File

@ -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<AssetWeakReference>(__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);