WIP: Brush assets project #106303

Draft
Julian Eisel wants to merge 351 commits from brush-assets-project into main

When changing the target branch, be careful to rebase the branch in your fork to match. See documentation.
10 changed files with 645 additions and 114 deletions
Showing only changes of commit 8e6dbddc5a - Show all commits

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);