Nodes: Add node group assets in add menu

This patch builds on the work from bdb5754147 to add node group
assets directly in the node editor add menu. Assets are added after
separators to distinguish them, but otherwise they look like any other
node. The catalog trees from all configured libraries are used to build
the menu hierarchy. Only catalogs with matching asset types are used
though.

There are a few limitations of this initial version. For now this only
supports geometry nodes. Support for other built-in node systems just
requires some refactoring of the corresponding add menu though. Lazy
loading will be added in a followup commit. For now there is a label
the first time the menu is opened.

Like the search menu integration, re-saving asset library files in 3.4
is required, if it hasn't been done already.

Implementation wise, there is a some ugly code here. A lot of that is
because the asset system isn't complete. The RNA API doesn't work well
yet, and the system isn't built to interact with multiple libraries at
once. It's also ugly because of the way we combine automatic menu
generation with builtin menus. As noted in a code comment, these two
systems could be merged completely so that the menus for builtin nodes
are also generated in the same way.

Differential Revision: https://developer.blender.org/D16135
This commit is contained in:
Hans Goudey 2022-11-01 16:09:49 +01:00
parent e6823f32e9
commit cf98518055
Notes: blender-bot 2023-08-04 01:04:31 +02:00
Referenced by commit 78a7d5cfcc, UI: Support C defined menu types to listen to notifiers
Referenced by commit 99e5024e97, UI: Support refreshing menu popups
Referenced by issue #102311, Regression: Crash when opening a file from a directory that is part of the asset library
Referenced by issue #92825, Refactor node editor add menu and search
Referenced by issue #106621, Regression: Accessing a GN submenu via Quick Favorites causes crash in
Referenced by commit 444a42e71c, Fix: Duplicate "Tool" geometry nodes menu with asset catalog
21 changed files with 619 additions and 28 deletions

View File

@ -58,6 +58,12 @@ def draw_node_group_add_menu(context, layout):
ops.name = "node_tree"
ops.value = "bpy.data.node_groups[%r]" % group.name
def draw_assets_for_catalog(layout, catalog_path):
layout.template_node_asset_menu_items(catalog_path=catalog_path)
def draw_root_assets(layout):
layout.menu_contents("NODE_MT_node_add_root_catalogs")
classes = (
)

View File

@ -16,6 +16,7 @@ class NODE_MT_geometry_node_GEO_ATTRIBUTE(Menu):
node_add_menu.add_node_type(layout, "GeometryNodeAttributeDomainSize")
node_add_menu.add_node_type(layout, "GeometryNodeRemoveAttribute")
node_add_menu.add_node_type(layout, "GeometryNodeStoreNamedAttribute")
node_add_menu.draw_assets_for_catalog(layout, self.bl_label)
class NODE_MT_geometry_node_GEO_COLOR(Menu):
@ -32,6 +33,7 @@ class NODE_MT_geometry_node_GEO_COLOR(Menu):
ops.value = "'RGBA'"
node_add_menu.add_node_type(layout, "ShaderNodeRGBCurve")
node_add_menu.add_node_type(layout, "FunctionNodeSeparateColor")
node_add_menu.draw_assets_for_catalog(layout, self.bl_label)
class NODE_MT_geometry_node_GEO_CURVE(Menu):
@ -70,6 +72,7 @@ class NODE_MT_geometry_node_GEO_CURVE(Menu):
node_add_menu.add_node_type(layout, "GeometryNodeSetSplineCyclic")
node_add_menu.add_node_type(layout, "GeometryNodeSetSplineResolution")
node_add_menu.add_node_type(layout, "GeometryNodeCurveSplineType")
node_add_menu.draw_assets_for_catalog(layout, self.bl_label)
class NODE_MT_geometry_node_GEO_PRIMITIVES_CURVE(Menu):
@ -86,6 +89,7 @@ class NODE_MT_geometry_node_GEO_PRIMITIVES_CURVE(Menu):
node_add_menu.add_node_type(layout, "GeometryNodeCurveQuadraticBezier")
node_add_menu.add_node_type(layout, "GeometryNodeCurvePrimitiveQuadrilateral")
node_add_menu.add_node_type(layout, "GeometryNodeCurveStar")
node_add_menu.draw_assets_for_catalog(layout, self.bl_label)
class NODE_MT_geometry_node_curve_topology(Menu):
@ -97,6 +101,7 @@ class NODE_MT_geometry_node_curve_topology(Menu):
node_add_menu.add_node_type(layout, "GeometryNodeOffsetPointInCurve")
node_add_menu.add_node_type(layout, "GeometryNodeCurveOfPoint")
node_add_menu.add_node_type(layout, "GeometryNodePointsOfCurve")
node_add_menu.draw_assets_for_catalog(layout, self.bl_label)
class NODE_MT_geometry_node_GEO_GEOMETRY(Menu):
@ -122,6 +127,7 @@ class NODE_MT_geometry_node_GEO_GEOMETRY(Menu):
layout.separator()
node_add_menu.add_node_type(layout, "GeometryNodeSetID")
node_add_menu.add_node_type(layout, "GeometryNodeSetPosition")
node_add_menu.draw_assets_for_catalog(layout, self.bl_label)
class NODE_MT_geometry_node_GEO_INPUT(Menu):
@ -149,6 +155,7 @@ class NODE_MT_geometry_node_GEO_INPUT(Menu):
node_add_menu.add_node_type(layout, "GeometryNodeInputPosition")
node_add_menu.add_node_type(layout, "GeometryNodeInputRadius")
node_add_menu.add_node_type(layout, "GeometryNodeInputSceneTime")
node_add_menu.draw_assets_for_catalog(layout, self.bl_label)
class NODE_MT_geometry_node_GEO_INSTANCE(Menu):
@ -166,6 +173,7 @@ class NODE_MT_geometry_node_GEO_INSTANCE(Menu):
layout.separator()
node_add_menu.add_node_type(layout, "GeometryNodeInputInstanceRotation")
node_add_menu.add_node_type(layout, "GeometryNodeInputInstanceScale")
node_add_menu.draw_assets_for_catalog(layout, self.bl_label)
class NODE_MT_geometry_node_GEO_MATERIAL(Menu):
@ -181,6 +189,7 @@ class NODE_MT_geometry_node_GEO_MATERIAL(Menu):
layout.separator()
node_add_menu.add_node_type(layout, "GeometryNodeSetMaterial")
node_add_menu.add_node_type(layout, "GeometryNodeSetMaterialIndex")
node_add_menu.draw_assets_for_catalog(layout, self.bl_label)
class NODE_MT_geometry_node_GEO_MESH(Menu):
@ -219,6 +228,7 @@ class NODE_MT_geometry_node_GEO_MESH(Menu):
node_add_menu.add_node_type(layout, "GeometryNodeInputMeshVertexNeighbors")
layout.separator()
node_add_menu.add_node_type(layout, "GeometryNodeSetShadeSmooth")
node_add_menu.draw_assets_for_catalog(layout, self.bl_label)
class NODE_MT_category_PRIMITIVES_MESH(Menu):
@ -235,6 +245,7 @@ class NODE_MT_category_PRIMITIVES_MESH(Menu):
node_add_menu.add_node_type(layout, "GeometryNodeMeshCircle")
node_add_menu.add_node_type(layout, "GeometryNodeMeshLine")
node_add_menu.add_node_type(layout, "GeometryNodeMeshUVSphere")
node_add_menu.draw_assets_for_catalog(layout, self.bl_label)
class NODE_MT_geometry_node_mesh_topology(Menu):
@ -250,6 +261,7 @@ class NODE_MT_geometry_node_mesh_topology(Menu):
node_add_menu.add_node_type(layout, "GeometryNodeFaceOfCorner"),
node_add_menu.add_node_type(layout, "GeometryNodeOffsetCornerInFace"),
node_add_menu.add_node_type(layout, "GeometryNodeVertexOfCorner"),
node_add_menu.draw_assets_for_catalog(layout, self.bl_label)
class NODE_MT_category_GEO_OUTPUT(Menu):
@ -259,6 +271,7 @@ class NODE_MT_category_GEO_OUTPUT(Menu):
def draw(self, _context):
layout = self.layout
node_add_menu.add_node_type(layout, "GeometryNodeViewer")
node_add_menu.draw_assets_for_catalog(layout, self.bl_label)
class NODE_MT_category_GEO_POINT(Menu):
@ -274,6 +287,7 @@ class NODE_MT_category_GEO_POINT(Menu):
node_add_menu.add_node_type(layout, "GeometryNodePointsToVolume")
layout.separator()
node_add_menu.add_node_type(layout, "GeometryNodeSetPointRadius")
node_add_menu.draw_assets_for_catalog(layout, self.bl_label)
class NODE_MT_category_GEO_TEXT(Menu):
@ -290,6 +304,7 @@ class NODE_MT_category_GEO_TEXT(Menu):
node_add_menu.add_node_type(layout, "FunctionNodeValueToString")
layout.separator()
node_add_menu.add_node_type(layout, "FunctionNodeInputSpecialCharacters")
node_add_menu.draw_assets_for_catalog(layout, self.bl_label)
class NODE_MT_category_GEO_TEXTURE(Menu):
@ -308,6 +323,7 @@ class NODE_MT_category_GEO_TEXTURE(Menu):
node_add_menu.add_node_type(layout, "ShaderNodeTexVoronoi")
node_add_menu.add_node_type(layout, "ShaderNodeTexWave")
node_add_menu.add_node_type(layout, "ShaderNodeTexWhiteNoise")
node_add_menu.draw_assets_for_catalog(layout, self.bl_label)
class NODE_MT_category_GEO_UTILITIES(Menu):
@ -331,6 +347,7 @@ class NODE_MT_category_GEO_UTILITIES(Menu):
node_add_menu.add_node_type(layout, "FunctionNodeRandomValue")
node_add_menu.add_node_type(layout, "FunctionNodeRotateEuler")
node_add_menu.add_node_type(layout, "GeometryNodeSwitch")
node_add_menu.draw_assets_for_catalog(layout, self.bl_label)
class NODE_MT_category_GEO_UV(Menu):
@ -341,6 +358,7 @@ class NODE_MT_category_GEO_UV(Menu):
layout = self.layout
node_add_menu.add_node_type(layout, "GeometryNodeUVPackIslands")
node_add_menu.add_node_type(layout, "GeometryNodeUVUnwrap")
node_add_menu.draw_assets_for_catalog(layout, self.bl_label)
class NODE_MT_category_GEO_VECTOR(Menu):
@ -354,6 +372,7 @@ class NODE_MT_category_GEO_VECTOR(Menu):
node_add_menu.add_node_type(layout, "ShaderNodeVectorCurve")
node_add_menu.add_node_type(layout, "ShaderNodeVectorMath")
node_add_menu.add_node_type(layout, "ShaderNodeVectorRotate")
node_add_menu.draw_assets_for_catalog(layout, self.bl_label)
class NODE_MT_category_GEO_VOLUME(Menu):
@ -364,6 +383,7 @@ class NODE_MT_category_GEO_VOLUME(Menu):
layout = self.layout
node_add_menu.add_node_type(layout, "GeometryNodeVolumeCube")
node_add_menu.add_node_type(layout, "GeometryNodeVolumeToMesh")
node_add_menu.draw_assets_for_catalog(layout, self.bl_label)
class NODE_MT_category_GEO_GROUP(Menu):
@ -373,6 +393,7 @@ class NODE_MT_category_GEO_GROUP(Menu):
def draw(self, context):
layout = self.layout
node_add_menu.draw_node_group_add_menu(context, layout)
node_add_menu.draw_assets_for_catalog(layout, self.bl_label)
class NODE_MT_category_GEO_LAYOUT(Menu):
@ -383,6 +404,7 @@ class NODE_MT_category_GEO_LAYOUT(Menu):
layout = self.layout
node_add_menu.add_node_type(layout, "NodeFrame")
node_add_menu.add_node_type(layout, "NodeReroute")
node_add_menu.draw_assets_for_catalog(layout, self.bl_label)
class NODE_MT_geometry_node_add_all(Menu):
@ -413,6 +435,7 @@ class NODE_MT_geometry_node_add_all(Menu):
layout.menu("NODE_MT_category_GEO_VOLUME")
layout.menu("NODE_MT_category_GEO_GROUP")
layout.menu("NODE_MT_category_GEO_LAYOUT")
node_add_menu.draw_root_assets(layout)
classes = (

View File

@ -307,7 +307,7 @@ class AssetCatalogTreeItem {
/** Iterate over children calling \a callback for each of them, but do not recurse into their
* children. */
void foreach_child(const ItemIterFn callback);
void foreach_child(ItemIterFn callback);
protected:
/** Child tree items, ordered by their names. */
@ -345,10 +345,15 @@ class AssetCatalogTree {
/** Ensure an item representing \a path is in the tree, adding it if necessary. */
void insert_item(const AssetCatalog &catalog);
void foreach_item(const AssetCatalogTreeItem::ItemIterFn callback);
void foreach_item(ItemIterFn callback);
/** Iterate over root items calling \a callback for each of them, but do not recurse into their
* children. */
void foreach_root_item(const ItemIterFn callback);
void foreach_root_item(ItemIterFn callback);
bool is_empty() const;
AssetCatalogTreeItem *find_item(const AssetCatalogPath &path);
AssetCatalogTreeItem *find_root_item(const AssetCatalogPath &path);
protected:
/** Child tree items, ordered by their names. */

View File

@ -49,7 +49,7 @@ class AssetCatalogPath {
AssetCatalogPath() = default;
AssetCatalogPath(StringRef path);
AssetCatalogPath(const std::string &path);
AssetCatalogPath(std::string path);
AssetCatalogPath(const char *path);
AssetCatalogPath(const AssetCatalogPath &other_path) = default;
AssetCatalogPath(AssetCatalogPath &&other_path) noexcept;

View File

@ -10,6 +10,8 @@
# error This is a C++-only header file. Use BKE_asset_library.h instead.
#endif
#include "DNA_asset_types.h"
#include "BKE_asset_library.h"
#include "BKE_asset_catalog.hh"
@ -44,19 +46,24 @@ struct AssetLibrary {
* No-op if the catalog cannot be found. This could be the kind of "the
* catalog definition file is corrupt/lost" scenario that the simple name is
* meant to help recover from. */
void refresh_catalog_simplename(struct AssetMetaData *asset_data);
void refresh_catalog_simplename(AssetMetaData *asset_data);
void on_blend_save_handler_register();
void on_blend_save_handler_unregister();
void on_blend_save_post(struct Main *, struct PointerRNA **pointers, int num_pointers);
void on_blend_save_post(Main *bmain, PointerRNA **pointers, int num_pointers);
private:
bCallbackFuncStore on_save_callback_store_{};
};
Vector<AssetLibraryReference> all_valid_asset_library_refs();
} // namespace blender::bke
blender::bke::AssetLibrary *BKE_asset_library_load(const Main *bmain,
const AssetLibraryReference &library_reference);
blender::bke::AssetCatalogService *BKE_asset_library_get_catalog_service(
const ::AssetLibrary *library);
blender::bke::AssetCatalogTree *BKE_asset_library_get_catalog_tree(const ::AssetLibrary *library);

View File

@ -9,6 +9,7 @@
#include "BKE_asset_catalog.hh"
#include "BKE_asset_library.h"
#include "BKE_asset_library.hh"
#include "BLI_fileops.hh"
#include "BLI_path_util.h"
@ -787,6 +788,41 @@ void AssetCatalogTree::foreach_root_item(const ItemIterFn callback)
}
}
bool AssetCatalogTree::is_empty() const
{
return root_items_.empty();
}
AssetCatalogTreeItem *AssetCatalogTree::find_item(const AssetCatalogPath &path)
{
AssetCatalogTreeItem *result = nullptr;
this->foreach_item([&](AssetCatalogTreeItem &item) {
if (result) {
/* There is no way to stop iteration. */
return;
}
if (item.catalog_path() == path) {
result = &item;
}
});
return result;
}
AssetCatalogTreeItem *AssetCatalogTree::find_root_item(const AssetCatalogPath &path)
{
AssetCatalogTreeItem *result = nullptr;
this->foreach_root_item([&](AssetCatalogTreeItem &item) {
if (result) {
/* There is no way to stop iteration. */
return;
}
if (item.catalog_path() == path) {
result = &item;
}
});
return result;
}
/* ---------------------------------------------------------------------- */
/* ---------------------------------------------------------------------- */

View File

@ -12,7 +12,7 @@ namespace blender::bke {
const char AssetCatalogPath::SEPARATOR = '/';
AssetCatalogPath::AssetCatalogPath(const std::string &path) : path_(path)
AssetCatalogPath::AssetCatalogPath(std::string path) : path_(std::move(path))
{
}

View File

@ -10,6 +10,7 @@
#include "BKE_main.h"
#include "BKE_preferences.h"
#include "BLI_fileops.h"
#include "BLI_path_util.h"
#include "DNA_asset_types.h"
@ -19,6 +20,13 @@
bool blender::bke::AssetLibrary::save_catalogs_when_file_is_saved = true;
blender::bke::AssetLibrary *BKE_asset_library_load(const Main *bmain,
const AssetLibraryReference &library_reference)
{
blender::bke::AssetLibraryService *service = blender::bke::AssetLibraryService::get();
return service->get_asset_library(bmain, library_reference);
}
/**
* Loading an asset library at this point only means loading the catalogs. Later on this should
* invoke reading of asset representations too.
@ -172,4 +180,26 @@ void AssetLibrary::refresh_catalog_simplename(struct AssetMetaData *asset_data)
}
STRNCPY(asset_data->catalog_simple_name, catalog->simple_name.c_str());
}
Vector<AssetLibraryReference> all_valid_asset_library_refs()
{
Vector<AssetLibraryReference> result;
int i;
LISTBASE_FOREACH_INDEX (const bUserAssetLibrary *, asset_library, &U.asset_libraries, i) {
if (!BLI_is_dir(asset_library->path)) {
continue;
}
AssetLibraryReference library_ref{};
library_ref.custom_library_index = i;
library_ref.type = ASSET_LIBRARY_CUSTOM;
result.append(library_ref);
}
AssetLibraryReference library_ref{};
library_ref.custom_library_index = -1;
library_ref.type = ASSET_LIBRARY_LOCAL;
result.append(library_ref);
return result;
}
} // namespace blender::bke

View File

@ -7,11 +7,15 @@
#include "asset_library_service.hh"
#include "BKE_blender.h"
#include "BKE_preferences.h"
#include "BLI_fileops.h" /* For PATH_MAX (at least on Windows). */
#include "BLI_path_util.h"
#include "BLI_string_ref.hh"
#include "DNA_asset_types.h"
#include "DNA_userdef_types.h"
#include "CLG_log.h"
static CLG_LogRef LOG = {"bke.asset_service"};
@ -38,6 +42,38 @@ void AssetLibraryService::destroy()
instance_.reset();
}
AssetLibrary *AssetLibraryService::get_asset_library(
const Main *bmain, const AssetLibraryReference &library_reference)
{
if (library_reference.type == ASSET_LIBRARY_LOCAL) {
/* For the "Current File" library we get the asset library root path based on main. */
char root_path[FILE_MAX];
if (bmain) {
BKE_asset_library_find_suitable_root_path_from_main(bmain, root_path);
}
else {
root_path[0] = '\0';
}
if (root_path[0] == '\0') {
/* File wasn't saved yet. */
return get_asset_library_current_file();
}
return get_asset_library_on_disk(root_path);
}
if (library_reference.type == ASSET_LIBRARY_CUSTOM) {
bUserAssetLibrary *user_library = BKE_preferences_asset_library_find_from_index(
&U, library_reference.custom_library_index);
if (user_library) {
return get_asset_library_on_disk(user_library->path);
}
}
return nullptr;
}
namespace {
std::string normalize_directory_path(StringRefNull directory)
{

View File

@ -44,6 +44,9 @@ class AssetLibraryService {
/** Destroy the AssetLibraryService singleton. It will be reallocated by #get() if necessary. */
static void destroy();
AssetLibrary *get_asset_library(const Main *bmain,
const AssetLibraryReference &library_reference);
/**
* Get the given asset library. Opens it (i.e. creates a new AssetLibrary instance) if necessary.
*/

View File

@ -23,6 +23,7 @@ struct wmNotifier;
*/
void ED_assetlist_storage_fetch(const struct AssetLibraryReference *library_reference,
const struct bContext *C);
bool ED_assetlist_is_loaded(const struct AssetLibraryReference *library_reference);
void ED_assetlist_ensure_previews_job(const struct AssetLibraryReference *library_reference,
const struct bContext *C);
void ED_assetlist_clear(const struct AssetLibraryReference *library_reference, struct bContext *C);

View File

@ -422,6 +422,18 @@ void ED_assetlist_storage_fetch(const AssetLibraryReference *library_reference,
AssetListStorage::fetch_library(*library_reference, *C);
}
bool ED_assetlist_is_loaded(const AssetLibraryReference *library_reference)
{
AssetList *list = AssetListStorage::lookup_list(*library_reference);
if (!list) {
return false;
}
if (list->needsRefetch()) {
return false;
}
return true;
}
void ED_assetlist_ensure_previews_job(const AssetLibraryReference *library_reference,
const bContext *C)
{

View File

@ -2525,6 +2525,7 @@ void uiTemplateNodeView(uiLayout *layout,
struct bNodeTree *ntree,
struct bNode *node,
struct bNodeSocket *input);
void uiTemplateNodeAssetMenuItems(uiLayout *layout, struct bContext *C, const char *catalog_path);
void uiTemplateTextureUser(uiLayout *layout, struct bContext *C);
/**
* Button to quickly show texture in Properties Editor texture tab.

View File

@ -29,6 +29,7 @@ set(INC
set(SRC
add_node_search.cc
add_menu_assets.cc
drawnode.cc
link_drag_search.cc
node_add.cc

View File

@ -0,0 +1,297 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BLI_multi_value_map.hh"
#include "DNA_screen_types.h"
#include "DNA_space_types.h"
#include "BKE_asset.h"
#include "BKE_asset_catalog.hh"
#include "BKE_asset_library.hh"
#include "BKE_idprop.h"
#include "BKE_screen.h"
#include "BLT_translation.h"
#include "RNA_access.h"
#include "RNA_prototypes.h"
#include "ED_asset.h"
#include "node_intern.hh"
namespace blender::ed::space_node {
static bool node_add_menu_poll(const bContext *C, MenuType * /*mt*/)
{
return CTX_wm_space_node(C);
}
struct LibraryAsset {
AssetLibraryReference library_ref;
AssetHandle handle;
};
struct LibraryCatalog {
bke::AssetLibrary *library;
const bke::AssetCatalog *catalog;
};
struct AssetItemTree {
bke::AssetCatalogTree catalogs;
MultiValueMap<bke::AssetCatalogPath, LibraryAsset> assets_per_path;
Map<const bke::AssetCatalogTreeItem *, bke::AssetCatalogPath> full_catalog_per_tree_item;
};
static bool all_loading_finished()
{
for (const AssetLibraryReference &library : bke::all_valid_asset_library_refs()) {
if (!ED_assetlist_is_loaded(&library)) {
return false;
}
}
return true;
}
static AssetItemTree build_catalog_tree(const bContext &C, const bNodeTree *node_tree)
{
if (!node_tree) {
return {};
}
const Main &bmain = *CTX_data_main(&C);
const Vector<AssetLibraryReference> all_libraries = bke::all_valid_asset_library_refs();
/* Merge catalogs from all libraries to deduplicate menu items. Also store the catalog and
* library for each asset ID in order to use them later when retrieving assets and removing
* empty catalogs. */
Map<bke::CatalogID, LibraryCatalog> id_to_catalog_map;
bke::AssetCatalogTree catalogs_from_all_libraries;
for (const AssetLibraryReference &library_ref : all_libraries) {
if (bke::AssetLibrary *library = BKE_asset_library_load(&bmain, library_ref)) {
if (bke::AssetCatalogTree *tree = library->catalog_service->get_catalog_tree()) {
tree->foreach_item([&](bke::AssetCatalogTreeItem &item) {
const bke::CatalogID &id = item.get_catalog_id();
bke::AssetCatalog *catalog = library->catalog_service->find_catalog(id);
catalogs_from_all_libraries.insert_item(*catalog);
id_to_catalog_map.add(item.get_catalog_id(), LibraryCatalog{library, catalog});
});
}
}
}
/* Find all the matching node group assets for every catalog path. */
MultiValueMap<bke::AssetCatalogPath, LibraryAsset> assets_per_path;
for (const AssetLibraryReference &library_ref : all_libraries) {
AssetFilterSettings type_filter{};
type_filter.id_types = FILTER_ID_NT;
ED_assetlist_storage_fetch(&library_ref, &C);
ED_assetlist_ensure_previews_job(&library_ref, &C);
ED_assetlist_iterate(library_ref, [&](AssetHandle asset) {
if (!ED_asset_filter_matches_asset(&type_filter, &asset)) {
return true;
}
const AssetMetaData &meta_data = *ED_asset_handle_get_metadata(&asset);
const IDProperty *tree_type = BKE_asset_metadata_idprop_find(&meta_data, "type");
if (tree_type == nullptr || IDP_Int(tree_type) != node_tree->type) {
return true;
}
if (BLI_uuid_is_nil(meta_data.catalog_id)) {
return true;
}
const LibraryCatalog &library_catalog = id_to_catalog_map.lookup(meta_data.catalog_id);
assets_per_path.add(library_catalog.catalog->path, LibraryAsset{library_ref, asset});
return true;
});
}
/* Build the final tree without any of the catalogs that don't have proper node group assets. */
bke::AssetCatalogTree catalogs_with_node_assets;
catalogs_from_all_libraries.foreach_item([&](bke::AssetCatalogTreeItem &item) {
if (!assets_per_path.lookup(item.catalog_path()).is_empty()) {
const bke::CatalogID &id = item.get_catalog_id();
const LibraryCatalog &library_catalog = id_to_catalog_map.lookup(id);
bke::AssetCatalog *catalog = library_catalog.library->catalog_service->find_catalog(id);
catalogs_with_node_assets.insert_item(*catalog);
}
});
/* Build another map storing full asset paths for each tree item, in order to have stable
* pointers to asset catalog paths to use for context pointers. This is necessary because
* #bke::AssetCatalogTreeItem doesn't store its full path directly. */
Map<const bke::AssetCatalogTreeItem *, bke::AssetCatalogPath> full_catalog_per_tree_item;
catalogs_with_node_assets.foreach_item([&](bke::AssetCatalogTreeItem &item) {
full_catalog_per_tree_item.add_new(&item, item.catalog_path());
});
return {std::move(catalogs_with_node_assets),
std::move(assets_per_path),
std::move(full_catalog_per_tree_item)};
}
static void node_add_catalog_assets_draw(const bContext *C, Menu *menu)
{
bScreen &screen = *CTX_wm_screen(C);
const SpaceNode &snode = *CTX_wm_space_node(C);
if (!snode.runtime->assets_for_menu) {
BLI_assert_unreachable();
return;
}
AssetItemTree &tree = *snode.runtime->assets_for_menu;
const bNodeTree *edit_tree = snode.edittree;
if (!edit_tree) {
return;
}
const PointerRNA menu_path_ptr = CTX_data_pointer_get(C, "asset_catalog_path");
if (RNA_pointer_is_null(&menu_path_ptr)) {
return;
}
const bke::AssetCatalogPath &menu_path = *static_cast<const bke::AssetCatalogPath *>(
menu_path_ptr.data);
const Span<LibraryAsset> asset_items = tree.assets_per_path.lookup(menu_path);
bke::AssetCatalogTreeItem *catalog_item = tree.catalogs.find_item(menu_path);
BLI_assert(catalog_item != nullptr);
if (asset_items.is_empty() && !catalog_item->has_children()) {
return;
}
uiLayout *layout = menu->layout;
uiItemS(layout);
for (const LibraryAsset &item : asset_items) {
uiLayout *col = uiLayoutColumn(layout, false);
PointerRNA file{
&screen.id, &RNA_FileSelectEntry, const_cast<FileDirEntry *>(item.handle.file_data)};
uiLayoutSetContextPointer(col, "active_file", &file);
PointerRNA library_ptr{&screen.id,
&RNA_AssetLibraryReference,
const_cast<AssetLibraryReference *>(&item.library_ref)};
uiLayoutSetContextPointer(col, "asset_library_ref", &library_ptr);
uiItemO(col, ED_asset_handle_get_name(&item.handle), ICON_NONE, "NODE_OT_add_group_asset");
}
catalog_item->foreach_child([&](bke::AssetCatalogTreeItem &child_item) {
const bke::AssetCatalogPath &path = tree.full_catalog_per_tree_item.lookup(&child_item);
PointerRNA path_ptr{
&screen.id, &RNA_AssetCatalogPath, const_cast<bke::AssetCatalogPath *>(&path)};
uiLayout *col = uiLayoutColumn(layout, false);
uiLayoutSetContextPointer(col, "asset_catalog_path", &path_ptr);
uiItemM(col, "NODE_MT_node_add_catalog_assets", path.name().c_str(), ICON_NONE);
});
}
static void add_root_catalogs_draw(const bContext *C, Menu *menu)
{
bScreen &screen = *CTX_wm_screen(C);
SpaceNode &snode = *CTX_wm_space_node(C);
const bNodeTree *edit_tree = snode.edittree;
uiLayout *layout = menu->layout;
snode.runtime->assets_for_menu = std::make_shared<AssetItemTree>(
build_catalog_tree(*C, edit_tree));
const bool loading_finished = all_loading_finished();
AssetItemTree &tree = *snode.runtime->assets_for_menu;
if (tree.catalogs.is_empty() && loading_finished) {
return;
}
uiItemS(layout);
if (!loading_finished) {
uiItemL(layout, IFACE_("Loading Asset Libraries"), ICON_INFO);
}
/* Avoid adding a separate root catalog when the assets have already been added to one of the
* builtin menus.
* TODO: The need to define the builtin menu labels here is completely non-ideal. We don't have
* any UI introspection that can do this though. This can be solved in the near future by
* removing the need to define the add menu completely, instead using a per-node-type path which
* can be merged with catalog tree.
*/
static Set<std::string> all_builtin_menus = []() {
Set<std::string> menus;
menus.add_new("Attribute");
menus.add_new("Color");
menus.add_new("Curve");
menus.add_new("Curve Primitives");
menus.add_new("Curve Topology");
menus.add_new("Geometry");
menus.add_new("Input");
menus.add_new("Instances");
menus.add_new("Material");
menus.add_new("Mesh");
menus.add_new("Mesh Primitives");
menus.add_new("Mesh Topology");
menus.add_new("Output");
menus.add_new("Point");
menus.add_new("Text");
menus.add_new("Texture");
menus.add_new("Utilities");
menus.add_new("UV");
menus.add_new("Vector");
menus.add_new("Volume");
menus.add_new("Group");
menus.add_new("Layout");
return menus;
}();
tree.catalogs.foreach_root_item([&](bke::AssetCatalogTreeItem &item) {
if (all_builtin_menus.contains(item.get_name())) {
return;
}
const bke::AssetCatalogPath &path = tree.full_catalog_per_tree_item.lookup(&item);
PointerRNA path_ptr{
&screen.id, &RNA_AssetCatalogPath, const_cast<bke::AssetCatalogPath *>(&path)};
uiLayout *col = uiLayoutColumn(layout, false);
uiLayoutSetContextPointer(col, "asset_catalog_path", &path_ptr);
uiItemM(col, "NODE_MT_node_add_catalog_assets", path.name().c_str(), ICON_NONE);
});
}
MenuType add_catalog_assets_menu_type()
{
MenuType type{};
BLI_strncpy(type.idname, "NODE_MT_node_add_catalog_assets", sizeof(type.idname));
type.poll = node_add_menu_poll;
type.draw = node_add_catalog_assets_draw;
return type;
}
MenuType add_root_catalogs_menu_type()
{
MenuType type{};
BLI_strncpy(type.idname, "NODE_MT_node_add_root_catalogs", sizeof(type.idname));
type.poll = node_add_menu_poll;
type.draw = add_root_catalogs_draw;
return type;
}
} // namespace blender::ed::space_node
/* Note: This is only necessary because Python can't set an asset catalog path context item. */
void uiTemplateNodeAssetMenuItems(uiLayout *layout, bContext *C, const char *catalog_path)
{
using namespace blender;
using namespace blender::ed::space_node;
bScreen &screen = *CTX_wm_screen(C);
SpaceNode &snode = *CTX_wm_space_node(C);
AssetItemTree &tree = *snode.runtime->assets_for_menu;
const bke::AssetCatalogTreeItem *item = tree.catalogs.find_root_item(catalog_path);
if (!item) {
return;
}
const bke::AssetCatalogPath &path = tree.full_catalog_per_tree_item.lookup(item);
PointerRNA path_ptr{
&screen.id, &RNA_AssetCatalogPath, const_cast<bke::AssetCatalogPath *>(&path)};
uiItemS(layout);
uiLayout *col = uiLayoutColumn(layout, false);
uiLayoutSetContextPointer(col, "asset_catalog_path", &path_ptr);
uiItemMContents(col, "NODE_MT_node_add_catalog_assets");
}

View File

@ -30,6 +30,7 @@
#include "DEG_depsgraph_build.h"
#include "ED_asset.h"
#include "ED_node.h" /* own include */
#include "ED_render.h"
#include "ED_screen.h"
@ -244,38 +245,36 @@ void NODE_OT_add_reroute(wmOperatorType *ot)
/** \name Add Node Group Operator
* \{ */
static bNodeTree *node_add_group_get_and_poll_group_node_tree(Main *bmain,
wmOperator *op,
bNodeTree *ntree)
static bool node_group_add_poll(const bNodeTree &node_tree,
const bNodeTree &node_group,
ReportList &reports)
{
bNodeTree *node_group = reinterpret_cast<bNodeTree *>(
WM_operator_properties_id_lookup_from_name_or_session_uuid(bmain, op->ptr, ID_NT));
if (!node_group) {
return nullptr;
if (node_group.type != node_tree.type) {
return false;
}
const char *disabled_hint = nullptr;
if ((node_group->type != ntree->type) || !nodeGroupPoll(ntree, node_group, &disabled_hint)) {
if (!nodeGroupPoll(&node_tree, &node_group, &disabled_hint)) {
if (disabled_hint) {
BKE_reportf(op->reports,
BKE_reportf(&reports,
RPT_ERROR,
"Can not add node group '%s' to '%s':\n %s",
node_group->id.name + 2,
ntree->id.name + 2,
node_group.id.name + 2,
node_tree.id.name + 2,
disabled_hint);
}
else {
BKE_reportf(op->reports,
BKE_reportf(&reports,
RPT_ERROR,
"Can not add node group '%s' to '%s'",
node_group->id.name + 2,
ntree->id.name + 2);
node_group.id.name + 2,
node_tree.id.name + 2);
}
return nullptr;
return false;
}
return node_group;
return true;
}
static int node_add_group_exec(bContext *C, wmOperator *op)
@ -284,10 +283,14 @@ static int node_add_group_exec(bContext *C, wmOperator *op)
SpaceNode *snode = CTX_wm_space_node(C);
bNodeTree *ntree = snode->edittree;
bNodeTree *node_group = node_add_group_get_and_poll_group_node_tree(bmain, op, ntree);
bNodeTree *node_group = reinterpret_cast<bNodeTree *>(
WM_operator_properties_id_lookup_from_name_or_session_uuid(bmain, op->ptr, ID_NT));
if (!node_group) {
return OPERATOR_CANCELLED;
}
if (!node_group_add_poll(*ntree, *node_group, *op->reports)) {
return OPERATOR_CANCELLED;
}
ED_preview_kill_jobs(CTX_wm_manager(C), CTX_data_main(C));
@ -320,9 +323,8 @@ static bool node_add_group_poll(bContext *C)
}
const SpaceNode *snode = CTX_wm_space_node(C);
if (snode->edittree->type == NTREE_CUSTOM) {
CTX_wm_operator_poll_msg_set(C,
"This node editor displays a custom (Python defined) node tree. "
"Dropping node groups isn't supported for this");
CTX_wm_operator_poll_msg_set(
C, "Adding node groups isn't supported for custom (Python defined) node trees");
return false;
}
return true;
@ -366,6 +368,105 @@ void NODE_OT_add_group(wmOperatorType *ot)
/** \} */
/* -------------------------------------------------------------------- */
/** \name Add Node Group Asset Operator
* \{ */
static bool add_node_group_asset(const bContext &C,
const AssetLibraryReference &library_ref,
const AssetHandle asset,
ReportList &reports)
{
Main &bmain = *CTX_data_main(&C);
SpaceNode &snode = *CTX_wm_space_node(&C);
bNodeTree &edit_tree = *snode.edittree;
bNodeTree *node_group = reinterpret_cast<bNodeTree *>(
asset::get_local_id_from_asset_or_append_and_reuse(bmain, library_ref, asset));
if (!node_group) {
return false;
}
if (!node_group_add_poll(edit_tree, *node_group, reports)) {
/* Remove the node group if it was newly appended but can't be added to the tree. */
id_us_plus(&node_group->id);
BKE_id_free_us(&bmain, node_group);
return false;
}
ED_preview_kill_jobs(CTX_wm_manager(&C), CTX_data_main(&C));
bNode *group_node = add_node(
C, ntreeTypeFind(node_group->idname)->group_idname, snode.runtime->cursor);
if (!group_node) {
BKE_report(&reports, RPT_WARNING, "Could not add node group");
return false;
}
/* By default, don't show the data-block selector since it's not usually necessary for assets. */
group_node->flag &= ~NODE_OPTIONS;
group_node->id = &node_group->id;
id_us_plus(group_node->id);
BKE_ntree_update_tag_node_property(&edit_tree, group_node);
nodeSetActive(&edit_tree, group_node);
ED_node_tree_propagate_change(&C, &bmain, nullptr);
DEG_relations_tag_update(&bmain);
return true;
}
static int node_add_group_asset_invoke(bContext *C, wmOperator *op, const wmEvent *event)
{
ARegion &region = *CTX_wm_region(C);
SpaceNode &snode = *CTX_wm_space_node(C);
const AssetLibraryReference *library_ref = CTX_wm_asset_library_ref(C);
if (!library_ref) {
return OPERATOR_CANCELLED;
}
bool is_valid;
const AssetHandle handle = CTX_wm_asset_handle(C, &is_valid);
if (!is_valid) {
return OPERATOR_CANCELLED;
}
/* Convert mouse coordinates to v2d space. */
UI_view2d_region_to_view(&region.v2d,
event->mval[0],
event->mval[1],
&snode.runtime->cursor[0],
&snode.runtime->cursor[1]);
snode.runtime->cursor /= UI_DPI_FAC;
if (!add_node_group_asset(*C, *library_ref, handle, *op->reports)) {
return OPERATOR_CANCELLED;
}
wmOperatorType *ot = WM_operatortype_find("NODE_OT_translate_attach_remove_on_cancel", true);
BLI_assert(ot);
PointerRNA ptr;
WM_operator_properties_create_ptr(&ptr, ot);
WM_operator_name_call_ptr(C, ot, WM_OP_INVOKE_DEFAULT, &ptr, nullptr);
WM_operator_properties_free(&ptr);
return OPERATOR_FINISHED;
}
void NODE_OT_add_group_asset(wmOperatorType *ot)
{
ot->name = "Add Node Group Asset";
ot->description = "Add a node group asset to the active node tree";
ot->idname = "NODE_OT_add_group_asset";
ot->invoke = node_add_group_asset_invoke;
ot->poll = node_add_group_poll;
ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO | OPTYPE_INTERNAL;
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Add Node Object Operator
* \{ */
@ -393,7 +494,7 @@ static int node_add_object_exec(bContext *C, wmOperator *op)
bNodeSocket *sock = nodeFindSocket(object_node, SOCK_IN, "Object");
if (!sock) {
BKE_report(op->reports, RPT_WARNING, "Could not find node object socket");
BLI_assert_unreachable();
return OPERATOR_CANCELLED;
}

View File

@ -38,6 +38,8 @@ extern const char *node_context_dir[];
namespace blender::ed::space_node {
struct AssetItemTree;
/** Temporary data used in node link drag modal operator. */
struct bNodeLinkDrag {
/** Links dragged by the operator. */
@ -96,6 +98,15 @@ struct SpaceNode_Runtime {
/* XXX hack for translate_attach op-macros to pass data from transform op to insert_offset op */
/** Temporary data for node insert offset (in UI called Auto-offset). */
struct NodeInsertOfsData *iofsd;
/**
* Temporary data for node add menu in order to provide longer-term storage for context pointers.
* Recreated every time the root menu is opened. In the future this will be replaced with an "all
* libraries" cache in the asset system itself.
*
* Stored with a shared pointer so that it can be forward declared.
*/
std::shared_ptr<AssetItemTree> assets_for_menu;
};
enum NodeResizeDirection {
@ -253,6 +264,7 @@ bNode *add_static_node(const bContext &C, int type, const float2 &location);
void NODE_OT_add_reroute(wmOperatorType *ot);
void NODE_OT_add_search(wmOperatorType *ot);
void NODE_OT_add_group(wmOperatorType *ot);
void NODE_OT_add_group_asset(wmOperatorType *ot);
void NODE_OT_add_object(wmOperatorType *ot);
void NODE_OT_add_collection(wmOperatorType *ot);
void NODE_OT_add_file(wmOperatorType *ot);
@ -383,4 +395,9 @@ void invoke_node_link_drag_add_menu(bContext &C,
void invoke_add_node_search_menu(bContext &C, const float2 &cursor, bool use_transform);
/* add_menu_assets.cc */
MenuType add_catalog_assets_menu_type();
MenuType add_root_catalogs_menu_type();
} // namespace blender::ed::space_node

View File

@ -78,6 +78,7 @@ void node_operatortypes()
WM_operatortype_append(NODE_OT_add_search);
WM_operatortype_append(NODE_OT_add_group);
WM_operatortype_append(NODE_OT_add_group_asset);
WM_operatortype_append(NODE_OT_add_object);
WM_operatortype_append(NODE_OT_add_collection);
WM_operatortype_append(NODE_OT_add_file);

View File

@ -1170,5 +1170,8 @@ void ED_spacetype_node()
art->draw = node_toolbar_region_draw;
BLI_addhead(&st->regiontypes, art);
WM_menutype_add(MEM_new<MenuType>(__func__, add_catalog_assets_menu_type()));
WM_menutype_add(MEM_new<MenuType>(__func__, add_root_catalogs_menu_type()));
BKE_spacetype_register(st);
}

View File

@ -472,6 +472,12 @@ static void rna_def_asset_handle(BlenderRNA *brna)
rna_def_asset_handle_api(srna);
}
static void rna_def_asset_catalog_path(BlenderRNA *brna)
{
StructRNA *srna = RNA_def_struct(brna, "AssetCatalogPath", NULL);
RNA_def_struct_ui_text(srna, "Catalog Path", "");
}
static void rna_def_asset_library_reference(BlenderRNA *brna)
{
StructRNA *srna = RNA_def_struct(brna, "AssetLibraryReference", NULL);
@ -498,6 +504,7 @@ void RNA_def_asset(BlenderRNA *brna)
rna_def_asset_data(brna);
rna_def_asset_library_reference(brna);
rna_def_asset_handle(brna);
rna_def_asset_catalog_path(brna);
RNA_define_animate_sdna(true);
}

View File

@ -1785,6 +1785,10 @@ void RNA_api_ui_layout(StructRNA *srna)
parm = RNA_def_pointer(func, "socket", "NodeSocket", "", "");
RNA_def_parameter_flags(parm, 0, PARM_REQUIRED);
func = RNA_def_function(srna, "template_node_asset_menu_items", "uiTemplateNodeAssetMenuItems");
RNA_def_function_flag(func, FUNC_USE_CONTEXT);
parm = RNA_def_string(func, "catalog_path", NULL, 0, "", "");
func = RNA_def_function(srna, "template_texture_user", "uiTemplateTextureUser");
RNA_def_function_flag(func, FUNC_USE_CONTEXT);