Assets: Additions/fixes to the catalog system in preparation for the UI

* Fixes missing update of the catalog tree when adding catalogs.
* Adds iterators for the catalogs, needed for UI code.
* Store catalog ID in the catalog tree items, needed for UI code.
* Other smaller API additions for the UI.
* Improve comments and smaller cleanups.

New functions are covered with unit tests.

Differential Revision: https://developer.blender.org/D12618

Reviewed by: Sybren Stüvel
This commit is contained in:
Julian Eisel 2021-09-27 17:45:02 +02:00
parent 5bea5e25d5
commit 824733ea47
3 changed files with 343 additions and 50 deletions

View File

@ -86,6 +86,10 @@ class AssetCatalogService {
/** Return catalog with the given ID. Return nullptr if not found. */
AssetCatalog *find_catalog(CatalogID catalog_id);
/** Return first catalog with the given path. Return nullptr if not found. This is not an
* efficient call as it's just a linear search over the catalogs. */
AssetCatalog *find_catalog_by_path(const CatalogPath &path) const;
/** Create a catalog with some sensible auto-generated catalog ID.
* The catalog will be saved to the default catalog file.*/
AssetCatalog *create_catalog(const CatalogPath &catalog_path);
@ -124,48 +128,74 @@ class AssetCatalogService {
void rebuild_tree();
};
/**
* Representation of a catalog path in the #AssetCatalogTree.
*/
class AssetCatalogTreeItem {
friend class AssetCatalogService;
friend class AssetCatalogTree;
public:
/** Container for child items. Uses a #std::map to keep items ordered by their name (i.e. their
* last catalog component). */
using ChildMap = std::map<std::string, AssetCatalogTreeItem>;
using ItemIterFn = FunctionRef<void(const AssetCatalogTreeItem &)>;
using ItemIterFn = FunctionRef<void(AssetCatalogTreeItem &)>;
AssetCatalogTreeItem(StringRef name, const AssetCatalogTreeItem *parent = nullptr);
AssetCatalogTreeItem(StringRef name,
CatalogID catalog_id,
const AssetCatalogTreeItem *parent = nullptr);
CatalogID get_catalog_id() const;
StringRef get_name() const;
/** Return the full catalog path, defined as the name of this catalog prefixed by the full
* catalog path of its parent and a separator. */
CatalogPath catalog_path() const;
int count_parents() const;
bool has_children() const;
static void foreach_item_recursive(const ChildMap &children_, const ItemIterFn callback);
/** Iterate over children calling \a callback for each of them, but do not recurse into their
* children. */
void foreach_child(const ItemIterFn callback);
protected:
/** Child tree items, ordered by their names. */
ChildMap children_;
/** The user visible name of this component. */
CatalogPathComponent name_;
CatalogID catalog_id_;
/** Pointer back to the parent item. Used to reconstruct the hierarchy from an item (e.g. to
* build a path). */
const AssetCatalogTreeItem *parent_ = nullptr;
private:
static void foreach_item_recursive(ChildMap &children_, ItemIterFn callback);
};
/**
* A representation of the catalog paths as tree structure. Each component of the catalog tree is
* represented by a #AssetCatalogTreeItem.
* represented by an #AssetCatalogTreeItem. The last path component of an item is used as its name,
* which may also be shown to the user.
* An item can not have multiple children with the same name. That means the name uniquely
* identifies an item within its parent.
*
* There is no single root tree element, the #AssetCatalogTree instance itself represents the root.
*/
class AssetCatalogTree {
friend class AssetCatalogService;
using ChildMap = AssetCatalogTreeItem::ChildMap;
using ItemIterFn = AssetCatalogTreeItem::ItemIterFn;
public:
void foreach_item(const AssetCatalogTreeItem::ItemIterFn callback) const;
/** 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);
/** 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);
protected:
/** Child tree items, ordered by their names. */
AssetCatalogTreeItem::ChildMap children_;
ChildMap root_items_;
};
/** Keeps track of which catalogs are defined in a certain file on disk.

View File

@ -64,6 +64,17 @@ AssetCatalog *AssetCatalogService::find_catalog(CatalogID catalog_id)
return catalog_uptr_ptr->get();
}
AssetCatalog *AssetCatalogService::find_catalog_by_path(const CatalogPath &path) const
{
for (auto &catalog : catalogs_.values()) {
if (catalog->path == path) {
return catalog.get();
}
}
return nullptr;
}
void AssetCatalogService::delete_catalog(CatalogID catalog_id)
{
std::unique_ptr<AssetCatalog> *catalog_uptr_ptr = this->catalogs_.lookup_ptr(catalog_id);
@ -104,6 +115,12 @@ AssetCatalog *AssetCatalogService::create_catalog(const CatalogPath &catalog_pat
catalog_definition_file_->add_new(catalog_ptr);
}
/* The tree may not exist; this happens when no catalog definition file has been loaded yet. When
* the tree is created any in-memory catalogs will be added, so it doesn't need to happen now. */
if (catalog_tree_) {
catalog_tree_->insert_item(*catalog_ptr);
}
return catalog_ptr;
}
@ -268,34 +285,7 @@ std::unique_ptr<AssetCatalogTree> AssetCatalogService::read_into_tree()
/* Go through the catalogs, insert each path component into the tree where needed. */
for (auto &catalog : catalogs_.values()) {
const AssetCatalogTreeItem *parent = nullptr;
AssetCatalogTreeItem::ChildMap *insert_to_map = &tree->children_;
BLI_assert_msg(!ELEM(catalog->path[0], '/', '\\'),
"Malformed catalog path; should not start with a separator");
const char *next_slash_ptr;
/* Looks more complicated than it is, this just iterates over path components. E.g.
* "just/some/path" iterates over "just", then "some" then "path". */
for (const char *name_begin = catalog->path.data(); name_begin && name_begin[0];
/* Jump to one after the next slash if there is any. */
name_begin = next_slash_ptr ? next_slash_ptr + 1 : nullptr) {
next_slash_ptr = BLI_path_slash_find(name_begin);
/* Note that this won't be null terminated. */
StringRef component_name = next_slash_ptr ?
StringRef(name_begin, next_slash_ptr - name_begin) :
/* Last component in the path. */
name_begin;
/* Insert new tree element - if no matching one is there yet! */
auto [item, was_inserted] = insert_to_map->emplace(
component_name, AssetCatalogTreeItem(component_name, parent));
/* Walk further into the path (no matter if a new item was created or not). */
parent = &item->second;
insert_to_map = &item->second.children_;
}
tree->insert_item(*catalog);
}
return tree;
@ -306,11 +296,20 @@ void AssetCatalogService::rebuild_tree()
this->catalog_tree_ = read_into_tree();
}
AssetCatalogTreeItem::AssetCatalogTreeItem(StringRef name, const AssetCatalogTreeItem *parent)
: name_(name), parent_(parent)
/* ---------------------------------------------------------------------- */
AssetCatalogTreeItem::AssetCatalogTreeItem(StringRef name,
CatalogID catalog_id,
const AssetCatalogTreeItem *parent)
: name_(name), catalog_id_(catalog_id), parent_(parent)
{
}
CatalogID AssetCatalogTreeItem::get_catalog_id() const
{
return catalog_id_;
}
StringRef AssetCatalogTreeItem::get_name() const
{
return name_;
@ -334,20 +333,100 @@ int AssetCatalogTreeItem::count_parents() const
return i;
}
void AssetCatalogTree::foreach_item(const AssetCatalogTreeItem::ItemIterFn callback) const
bool AssetCatalogTreeItem::has_children() const
{
AssetCatalogTreeItem::foreach_item_recursive(children_, callback);
return !children_.empty();
}
void AssetCatalogTreeItem::foreach_item_recursive(const AssetCatalogTreeItem::ChildMap &children,
/* ---------------------------------------------------------------------- */
/**
* Iterate over path components, calling \a callback for each component. E.g. "just/some/path"
* iterates over "just", then "some" then "path".
*/
static void iterate_over_catalog_path_components(
const CatalogPath &path,
FunctionRef<void(StringRef component_name, bool is_last_component)> callback)
{
const char *next_slash_ptr;
for (const char *path_component = path.data(); path_component && path_component[0];
/* Jump to one after the next slash if there is any. */
path_component = next_slash_ptr ? next_slash_ptr + 1 : nullptr) {
next_slash_ptr = BLI_path_slash_find(path_component);
const bool is_last_component = next_slash_ptr == nullptr;
/* Note that this won't be null terminated. */
const StringRef component_name = is_last_component ?
path_component :
StringRef(path_component,
next_slash_ptr - path_component);
callback(component_name, is_last_component);
}
}
void AssetCatalogTree::insert_item(const AssetCatalog &catalog)
{
const AssetCatalogTreeItem *parent = nullptr;
/* The children for the currently iterated component, where the following component should be
* added to (if not there yet). */
AssetCatalogTreeItem::ChildMap *current_item_children = &root_items_;
BLI_assert_msg(!ELEM(catalog.path[0], '/', '\\'),
"Malformed catalog path; should not start with a separator");
const CatalogID nil_id{};
iterate_over_catalog_path_components(
catalog.path, [&](StringRef component_name, const bool is_last_component) {
/* Insert new tree element - if no matching one is there yet! */
auto [key_and_item, was_inserted] = current_item_children->emplace(
component_name,
AssetCatalogTreeItem(
component_name, is_last_component ? catalog.catalog_id : nil_id, parent));
AssetCatalogTreeItem &item = key_and_item->second;
/* If full path of this catalog already exists as parent path of a previously read catalog,
* we can ensure this tree item's UUID is set here. */
if (is_last_component && BLI_uuid_is_nil(item.catalog_id_)) {
item.catalog_id_ = catalog.catalog_id;
}
/* Walk further into the path (no matter if a new item was created or not). */
parent = &item;
current_item_children = &item.children_;
});
}
void AssetCatalogTree::foreach_item(AssetCatalogTreeItem::ItemIterFn callback)
{
AssetCatalogTreeItem::foreach_item_recursive(root_items_, callback);
}
void AssetCatalogTreeItem::foreach_item_recursive(AssetCatalogTreeItem::ChildMap &children,
const ItemIterFn callback)
{
for (const auto &[key, item] : children) {
for (auto &[key, item] : children) {
callback(item);
foreach_item_recursive(item.children_, callback);
}
}
void AssetCatalogTree::foreach_root_item(const ItemIterFn callback)
{
for (auto &[key, item] : root_items_) {
callback(item);
}
}
void AssetCatalogTreeItem::foreach_child(const ItemIterFn callback)
{
for (auto &[key, item] : children_) {
callback(item);
}
}
AssetCatalogTree *AssetCatalogService::get_catalog_tree()
{
return catalog_tree_.get();

View File

@ -92,6 +92,22 @@ class AssetCatalogTest : public testing::Test {
int parent_count;
};
void assert_expected_item(const CatalogPathInfo &expected_path,
const AssetCatalogTreeItem &actual_item)
{
char expected_filename[FILE_MAXFILE];
/* Is the catalog name as expected? "character", "Ellie", ... */
BLI_split_file_part(expected_path.name.data(), expected_filename, sizeof(expected_filename));
EXPECT_EQ(expected_filename, actual_item.get_name());
/* Does the computed number of parents match? */
EXPECT_EQ(expected_path.parent_count, actual_item.count_parents());
EXPECT_EQ(expected_path.name, actual_item.catalog_path());
}
/**
* Recursively iterate over all tree items using #AssetCatalogTree::foreach_item() and check if
* the items map exactly to \a expected_paths.
*/
void assert_expected_tree_items(AssetCatalogTree *tree,
const std::vector<CatalogPathInfo> &expected_paths)
{
@ -99,16 +115,43 @@ class AssetCatalogTest : public testing::Test {
tree->foreach_item([&](const AssetCatalogTreeItem &actual_item) {
ASSERT_LT(i, expected_paths.size())
<< "More catalogs in tree than expected; did not expect " << actual_item.catalog_path();
assert_expected_item(expected_paths[i], actual_item);
i++;
});
}
char expected_filename[FILE_MAXFILE];
/* Is the catalog name as expected? "character", "Ellie", ... */
BLI_split_file_part(
expected_paths[i].name.data(), expected_filename, sizeof(expected_filename));
EXPECT_EQ(expected_filename, actual_item.get_name());
/* Does the computed number of parents match? */
EXPECT_EQ(expected_paths[i].parent_count, actual_item.count_parents());
EXPECT_EQ(expected_paths[i].name, actual_item.catalog_path());
/**
* Iterate over the root items of \a tree and check if the items map exactly to \a
* expected_paths. Similar to #assert_expected_tree_items() but calls
* #AssetCatalogTree::foreach_root_item() instead of #AssetCatalogTree::foreach_item().
*/
void assert_expected_tree_root_items(AssetCatalogTree *tree,
const std::vector<CatalogPathInfo> &expected_paths)
{
int i = 0;
tree->foreach_root_item([&](const AssetCatalogTreeItem &actual_item) {
ASSERT_LT(i, expected_paths.size())
<< "More catalogs in tree root than expected; did not expect "
<< actual_item.catalog_path();
assert_expected_item(expected_paths[i], actual_item);
i++;
});
}
/**
* Iterate over the child items of \a parent_item and check if the items map exactly to \a
* expected_paths. Similar to #assert_expected_tree_items() but calls
* #AssetCatalogTreeItem::foreach_child() instead of #AssetCatalogTree::foreach_item().
*/
void assert_expected_tree_item_child_items(AssetCatalogTreeItem *parent_item,
const std::vector<CatalogPathInfo> &expected_paths)
{
int i = 0;
parent_item->foreach_child([&](const AssetCatalogTreeItem &actual_item) {
ASSERT_LT(i, expected_paths.size())
<< "More catalogs in tree item than expected; did not expect "
<< actual_item.catalog_path();
assert_expected_item(expected_paths[i], actual_item);
i++;
});
}
@ -156,6 +199,87 @@ TEST_F(AssetCatalogTest, load_single_file)
EXPECT_EQ("POSES_RUŽENA", poses_ruzena->simple_name);
}
TEST_F(AssetCatalogTest, insert_item_into_tree)
{
{
AssetCatalogTree tree;
std::unique_ptr<AssetCatalog> catalog_empty_path = AssetCatalog::from_path("");
tree.insert_item(*catalog_empty_path);
assert_expected_tree_items(&tree, {});
}
{
AssetCatalogTree tree;
std::unique_ptr<AssetCatalog> catalog = AssetCatalog::from_path("item");
tree.insert_item(*catalog);
assert_expected_tree_items(&tree, {{"item", 0}});
/* Insert child after parent already exists. */
std::unique_ptr<AssetCatalog> child_catalog = AssetCatalog::from_path("item/child");
tree.insert_item(*catalog);
assert_expected_tree_items(&tree, {{"item", 0}, {"item/child", 1}});
std::vector<CatalogPathInfo> expected_paths;
/* Test inserting multi-component sub-path. */
std::unique_ptr<AssetCatalog> grandgrandchild_catalog = AssetCatalog::from_path(
"item/child/grandchild/grandgrandchild");
tree.insert_item(*catalog);
expected_paths = {{"item", 0},
{"item/child", 1},
{"item/child/grandchild", 2},
{"item/child/grandchild/grandgrandchild", 3}};
assert_expected_tree_items(&tree, expected_paths);
std::unique_ptr<AssetCatalog> root_level_catalog = AssetCatalog::from_path("root level");
tree.insert_item(*catalog);
expected_paths = {{"item", 0},
{"item/child", 1},
{"item/child/grandchild", 2},
{"item/child/grandchild/grandgrandchild", 3},
{"root level", 0}};
assert_expected_tree_items(&tree, expected_paths);
}
{
AssetCatalogTree tree;
std::unique_ptr<AssetCatalog> catalog = AssetCatalog::from_path("item/child");
tree.insert_item(*catalog);
assert_expected_tree_items(&tree, {{"item", 0}, {"item/child", 1}});
}
{
AssetCatalogTree tree;
std::unique_ptr<AssetCatalog> catalog = AssetCatalog::from_path("white space");
tree.insert_item(*catalog);
assert_expected_tree_items(&tree, {{"white space", 0}});
}
{
AssetCatalogTree tree;
std::unique_ptr<AssetCatalog> catalog = AssetCatalog::from_path("/item/white space");
tree.insert_item(*catalog);
assert_expected_tree_items(&tree, {{"item", 0}, {"item/white space", 1}});
}
{
AssetCatalogTree tree;
std::unique_ptr<AssetCatalog> catalog_unicode_path = AssetCatalog::from_path("Ružena");
tree.insert_item(*catalog_unicode_path);
assert_expected_tree_items(&tree, {{"Ružena", 0}});
catalog_unicode_path = AssetCatalog::from_path("Ružena/Ružena");
tree.insert_item(*catalog_unicode_path);
assert_expected_tree_items(&tree, {{"Ružena", 0}, {"Ružena/Ružena", 1}});
}
}
TEST_F(AssetCatalogTest, load_single_file_into_tree)
{
AssetCatalogService service(asset_library_root_);
@ -182,6 +306,66 @@ TEST_F(AssetCatalogTest, load_single_file_into_tree)
assert_expected_tree_items(tree, expected_paths);
}
TEST_F(AssetCatalogTest, foreach_in_tree)
{
{
AssetCatalogTree tree{};
const std::vector<CatalogPathInfo> no_catalogs{};
assert_expected_tree_items(&tree, no_catalogs);
assert_expected_tree_root_items(&tree, no_catalogs);
/* Need a root item to check child items. */
std::unique_ptr<AssetCatalog> catalog = AssetCatalog::from_path("something");
tree.insert_item(*catalog);
tree.foreach_root_item([&no_catalogs, this](AssetCatalogTreeItem &item) {
assert_expected_tree_item_child_items(&item, no_catalogs);
});
}
AssetCatalogService service(asset_library_root_);
service.load_from_disk(asset_library_root_ + "/" + "blender_assets.cats.txt");
std::vector<CatalogPathInfo> expected_root_items{{"character", 0}, {"path", 0}};
AssetCatalogTree *tree = service.get_catalog_tree();
assert_expected_tree_root_items(tree, expected_root_items);
/* Test if the direct children of the root item are what's expected. */
std::vector<std::vector<CatalogPathInfo>> expected_root_child_items = {
/* Children of the "character" root item. */
{{"character/Ellie", 1}, {"character/Ružena", 1}},
/* Children of the "path" root item. */
{{"path/without", 1}},
};
int i = 0;
tree->foreach_root_item([&expected_root_child_items, &i, this](AssetCatalogTreeItem &item) {
assert_expected_tree_item_child_items(&item, expected_root_child_items[i]);
i++;
});
}
TEST_F(AssetCatalogTest, find_catalog_by_path)
{
TestableAssetCatalogService service(asset_library_root_);
service.load_from_disk(asset_library_root_ + "/" +
AssetCatalogService::DEFAULT_CATALOG_FILENAME);
AssetCatalog *catalog;
EXPECT_EQ(nullptr, service.find_catalog_by_path(""));
catalog = service.find_catalog_by_path("character/Ellie/poselib/white space");
EXPECT_NE(nullptr, catalog);
EXPECT_EQ(UUID_POSES_ELLIE_WHITESPACE, catalog->catalog_id);
catalog = service.find_catalog_by_path("character/Ružena/poselib");
EXPECT_NE(nullptr, catalog);
EXPECT_EQ(UUID_POSES_RUZENA, catalog->catalog_id);
/* "character/Ellie/poselib" is used by two catalogs. Check if it's using the first one. */
catalog = service.find_catalog_by_path("character/Ellie/poselib");
EXPECT_NE(nullptr, catalog);
EXPECT_EQ(UUID_POSES_ELLIE, catalog->catalog_id);
EXPECT_NE(UUID_POSES_ELLIE_TRAILING_SLASH, catalog->catalog_id);
}
TEST_F(AssetCatalogTest, write_single_file)
{
TestableAssetCatalogService service(asset_library_root_);