Assets: add Asset Catalog system

Catalogs work like directories on disk (without hard-/symlinks), in that
an asset is only contained in one catalog.

See T90066 for design considerations.

#### Known Limitations

Only a single catalog definition file (CDF), is supported, at
`${ASSET_LIBRARY_ROOT}/blender_assets.cats.txt`. In the future this is
to be expanded to support arbitrary CDFs (like one per blend file, one
per subdirectory, etc.).

The current implementation is based on the asset browser, which in
practice means that the asset browser owns the `AssetCatalogService`
instance for the selected asset library. In the future these instances
will be accessible via a less UI-bound asset system.

The UI is still very rudimentary, only showing the catalog ID for the
currently selected asset. Most notably, the loaded catalogs are not
shown yet. The UI is being implemented and will be merged soon.

#### Catalog Identifiers

Catalogs are internally identified by UUID. In older designs this was a
human-readable name, which has the problem that it has to be kept in
sync with its semantics (so when renaming a catalog from X to Y, the
UUID can be kept the same).

Since UUIDs don't communicate any human-readable information, the
mapping from catalog UUID to its path (stored in the Catalog Definition
File, CDF) is critical for understanding which asset is stored in which
human-readable catalog. To make this less critical, and to allow manual
data reconstruction after a CDF is lost/corrupted, each catalog also has
a "simple name" that's stored along with the UUID. This is also stored
on each asset, next to the catalog UUID.

#### Writing to Disk

Before saving asset catalogs to disk, the to-be-overwritten file gets
inspected. Any new catalogs that are found thre are loaded to memory
before writing the catalogs back to disk:

- Changed catalog path: in-memory data wins
- Catalogs deleted on disk: they are recreated based on in-memory data
- Catalogs deleted in memory: deleted on disk as well
- New catalogs on disk: are loaded and thus survive the overwriting

#### Tree Design

This implements the initial tree structure to load catalogs into. See
T90608, and the basic design in T90066.

Reviewed By: Severin

Maniphest Tasks: T91552

Differential Revision: https://developer.blender.org/D12589
This commit is contained in:
Sybren A. Stüvel 2021-09-23 14:56:45 +02:00 committed by Sybren A. Stüvel
parent 222fd1abf0
commit 9b12b23d0b
Notes: blender-bot 2023-02-14 05:12:59 +01:00
Referenced by issue #91552, Merge Asset Catalog Backend
19 changed files with 1755 additions and 0 deletions

View File

@ -691,10 +691,23 @@ class ASSETBROWSER_PT_metadata(asset_utils.AssetBrowserPanel, Panel):
if asset_file_handle.local_id:
# If the active file is an ID, use its name directly so renaming is possible from right here.
layout.prop(asset_file_handle.local_id, "name", text="")
col = layout.column(align=True)
col.label(text="Asset Catalog:")
col.prop(asset_file_handle.local_id.asset_data, "catalog_id", text="UUID")
col.prop(asset_file_handle.local_id.asset_data, "catalog_simple_name", text="Simple Name")
row = layout.row()
row.label(text="Source: Current File")
else:
layout.prop(asset_file_handle, "name", text="")
col = layout.column(align=True)
col.enabled = False
col.label(text="Asset Catalog:")
col.prop(asset_file_handle.asset_data, "catalog_id", text="UUID")
col.prop(asset_file_handle.asset_data, "catalog_simple_name", text="Simple Name")
col = layout.column(align=True) # Just to reduce margin.
col.label(text="Source:")
row = col.row()

View File

@ -22,6 +22,8 @@
#include "BLI_utildefines.h"
#include "DNA_asset_types.h"
#ifdef __cplusplus
extern "C" {
#endif
@ -46,6 +48,12 @@ struct AssetTagEnsureResult BKE_asset_metadata_tag_ensure(struct AssetMetaData *
const char *name);
void BKE_asset_metadata_tag_remove(struct AssetMetaData *asset_data, struct AssetTag *tag);
/** Clean up the catalog ID (whitespaces removed, length reduced, etc.) and assign it. */
void BKE_asset_metadata_catalog_id_clear(struct AssetMetaData *asset_data);
void BKE_asset_metadata_catalog_id_set(struct AssetMetaData *asset_data,
bUUID catalog_id,
const char *catalog_simple_name);
void BKE_asset_library_reference_init_default(struct AssetLibraryReference *library_ref);
struct PreviewImage *BKE_asset_metadata_preview_get_from_id(const struct AssetMetaData *asset_data,

View File

@ -0,0 +1,252 @@
/*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
/** \file
* \ingroup bke
*/
#pragma once
#ifndef __cplusplus
# error This is a C++ header. The C interface is yet to be implemented/designed.
#endif
#include "BLI_function_ref.hh"
#include "BLI_map.hh"
#include "BLI_string_ref.hh"
#include "BLI_uuid.h"
#include "BLI_vector.hh"
#include <map>
#include <memory>
#include <string>
namespace blender::bke {
using CatalogID = bUUID;
using CatalogPath = std::string;
using CatalogPathComponent = std::string;
/* Would be nice to be able to use `std::filesystem::path` for this, but it's currently not
* available on the minimum macOS target version. */
using CatalogFilePath = std::string;
class AssetCatalog;
class AssetCatalogDefinitionFile;
class AssetCatalogTree;
/* Manages the asset catalogs of a single asset library (i.e. of catalogs defined in a single
* directory hierarchy). */
class AssetCatalogService {
public:
static const char PATH_SEPARATOR;
static const CatalogFilePath DEFAULT_CATALOG_FILENAME;
public:
AssetCatalogService() = default;
explicit AssetCatalogService(const CatalogFilePath &asset_library_root);
/** Load asset catalog definitions from the files found in the asset library. */
void load_from_disk();
/** Load asset catalog definitions from the given file or directory. */
void load_from_disk(const CatalogFilePath &file_or_directory_path);
/**
* Write the catalog definitions to disk.
* The provided directory path is only used when there is no CDF loaded from disk yet but assets
* still have to be saved.
*
* Return true on success, which either means there were no in-memory categories to save, or the
* save was succesfful. */
bool write_to_disk(const CatalogFilePath &directory_for_new_files);
/**
* Merge on-disk changes into the in-memory asset catalogs.
* This should be called before writing the asset catalogs to disk.
*
* - New on-disk catalogs are loaded into memory.
* - Already-known on-disk catalogs are ignored (so will be overwritten with our in-memory
* data). This includes in-memory marked-as-deleted catalogs.
*/
void merge_from_disk_before_writing();
/** Return catalog with the given ID. Return nullptr if not found. */
AssetCatalog *find_catalog(CatalogID catalog_id);
/** 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);
/**
* Soft-delete the catalog, ensuring it actually gets deleted when the catalog definition file is
* written. */
void delete_catalog(CatalogID catalog_id);
AssetCatalogTree *get_catalog_tree();
/** Return true iff there are no catalogs known. */
bool is_empty() const;
protected:
/* These pointers are owned by this AssetCatalogService. */
Map<CatalogID, std::unique_ptr<AssetCatalog>> catalogs_;
Map<CatalogID, std::unique_ptr<AssetCatalog>> deleted_catalogs_;
std::unique_ptr<AssetCatalogDefinitionFile> catalog_definition_file_;
std::unique_ptr<AssetCatalogTree> catalog_tree_;
CatalogFilePath asset_library_root_;
void load_directory_recursive(const CatalogFilePath &directory_path);
void load_single_file(const CatalogFilePath &catalog_definition_file_path);
std::unique_ptr<AssetCatalogDefinitionFile> parse_catalog_file(
const CatalogFilePath &catalog_definition_file_path);
/**
* Construct an in-memory catalog definition file (CDF) from the currently known catalogs.
* This object can then be processed further before saving to disk. */
std::unique_ptr<AssetCatalogDefinitionFile> construct_cdf_in_memory(
const CatalogFilePath &file_path);
std::unique_ptr<AssetCatalogTree> read_into_tree();
void rebuild_tree();
};
class AssetCatalogTreeItem {
friend class AssetCatalogService;
public:
using ChildMap = std::map<std::string, AssetCatalogTreeItem>;
using ItemIterFn = FunctionRef<void(const AssetCatalogTreeItem &)>;
AssetCatalogTreeItem(StringRef name, const AssetCatalogTreeItem *parent = nullptr);
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;
static void foreach_item_recursive(const ChildMap &children_, const ItemIterFn callback);
protected:
/** Child tree items, ordered by their names. */
ChildMap children_;
/** The user visible name of this component. */
CatalogPathComponent name_;
/** Pointer back to the parent item. Used to reconstruct the hierarchy from an item (e.g. to
* build a path). */
const AssetCatalogTreeItem *parent_ = nullptr;
};
/**
* A representation of the catalog paths as tree structure. Each component of the catalog tree is
* represented by a #AssetCatalogTreeItem.
* There is no single root tree element, the #AssetCatalogTree instance itself represents the root.
*/
class AssetCatalogTree {
friend class AssetCatalogService;
public:
void foreach_item(const AssetCatalogTreeItem::ItemIterFn callback) const;
protected:
/** Child tree items, ordered by their names. */
AssetCatalogTreeItem::ChildMap children_;
};
/** Keeps track of which catalogs are defined in a certain file on disk.
* Only contains non-owning pointers to the #AssetCatalog instances, so ensure the lifetime of this
* class is shorter than that of the #`AssetCatalog`s themselves. */
class AssetCatalogDefinitionFile {
public:
CatalogFilePath file_path;
AssetCatalogDefinitionFile() = default;
/**
* Write the catalog definitions to the same file they were read from.
* Return true when the file was written correctly, false when there was a problem.
*/
bool write_to_disk() const;
/**
* Write the catalog definitions to an arbitrary file path.
*
* Any existing file is backed up to "filename~". Any previously existing backup is overwritten.
*
* Return true when the file was written correctly, false when there was a problem.
*/
bool write_to_disk(const CatalogFilePath &dest_file_path) const;
bool contains(CatalogID catalog_id) const;
/* Add a new catalog. Undefined behaviour if a catalog with the same ID was already added. */
void add_new(AssetCatalog *catalog);
using AssetCatalogParsedFn = FunctionRef<bool(std::unique_ptr<AssetCatalog>)>;
void parse_catalog_file(const CatalogFilePath &catalog_definition_file_path,
AssetCatalogParsedFn callback);
protected:
/* Catalogs stored in this file. They are mapped by ID to make it possible to query whether a
* catalog is already known, without having to find the corresponding `AssetCatalog*`. */
Map<CatalogID, AssetCatalog *> catalogs_;
std::unique_ptr<AssetCatalog> parse_catalog_line(StringRef line);
/**
* Write the catalog definitions to the given file path.
* Return true when the file was written correctly, false when there was a problem.
*/
bool write_to_disk_unsafe(const CatalogFilePath &dest_file_path) const;
bool ensure_directory_exists(const CatalogFilePath directory_path) const;
};
/** Asset Catalog definition, containing a symbolic ID and a path that points to a node in the
* catalog hierarchy. */
class AssetCatalog {
public:
AssetCatalog() = default;
AssetCatalog(CatalogID catalog_id, const CatalogPath &path, const std::string &simple_name);
CatalogID catalog_id;
CatalogPath path;
/**
* Simple, human-readable name for the asset catalog. This is stored on assets alongside the
* catalog ID; the catalog ID is a UUID that is not human-readable, so to avoid complete dataloss
* when the catalog definition file gets lost, we also store a human-readable simple name for the
* catalog. */
std::string simple_name;
struct Flags {
/* Treat this catalog as deleted. Keeping deleted catalogs around is necessary to support
* merging of on-disk changes with in-memory changes. */
bool is_deleted = false;
} flags;
/**
* Create a new Catalog with the given path, auto-generating a sensible catalog simplename.
*
* NOTE: the given path will be cleaned up (trailing spaces removed, etc.), so the returned
* `AssetCatalog`'s path differ from the given one.
*/
static std::unique_ptr<AssetCatalog> from_path(const CatalogPath &path);
static CatalogPath cleanup_path(const CatalogPath &path);
protected:
/** Generate a sensible catalog ID for the given path. */
static std::string sensible_simple_name_for_path(const CatalogPath &path);
};
} // namespace blender::bke

View File

@ -0,0 +1,36 @@
/*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
/** \file
* \ingroup bke
*/
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
/** Forward declaration, defined in intern/asset_library.hh */
typedef struct AssetLibrary AssetLibrary;
/** TODO(@sybren): properly have a think/discussion about the API for this. */
struct AssetLibrary *BKE_asset_library_load(const char *library_path);
void BKE_asset_library_free(struct AssetLibrary *asset_library);
#ifdef __cplusplus
}
#endif

View File

@ -0,0 +1,41 @@
/*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
/** \file
* \ingroup bke
*/
#pragma once
#ifndef __cplusplus
# error This is a C++-only header file. Use BKE_asset_library.h instead.
#endif
#include "BKE_asset_library.h"
#include "BKE_asset_catalog.hh"
#include <memory>
namespace blender::bke {
struct AssetLibrary {
std::unique_ptr<AssetCatalogService> catalog_service;
void load(StringRefNull library_root_directory);
};
} // namespace blender::bke

View File

@ -83,6 +83,8 @@ set(SRC
intern/armature_pose.cc
intern/armature_selection.cc
intern/armature_update.c
intern/asset_catalog.cc
intern/asset_library.cc
intern/asset.cc
intern/attribute.c
intern/attribute_access.cc
@ -302,6 +304,9 @@ set(SRC
BKE_appdir.h
BKE_armature.h
BKE_armature.hh
BKE_asset_catalog.hh
BKE_asset_library.h
BKE_asset_library.hh
BKE_asset.h
BKE_attribute.h
BKE_attribute_access.hh
@ -783,6 +788,9 @@ if(WITH_GTESTS)
set(TEST_SRC
intern/action_test.cc
intern/armature_test.cc
intern/asset_catalog_test.cc
intern/asset_library_test.cc
intern/asset_test.cc
intern/cryptomatte_test.cc
intern/fcurve_test.cc
intern/lattice_deform_test.cc

View File

@ -26,8 +26,10 @@
#include "BLI_listbase.h"
#include "BLI_string.h"
#include "BLI_string_ref.hh"
#include "BLI_string_utils.h"
#include "BLI_utildefines.h"
#include "BLI_uuid.h"
#include "BKE_asset.h"
#include "BKE_icons.h"
@ -37,6 +39,8 @@
#include "MEM_guardedalloc.h"
using namespace blender;
AssetMetaData *BKE_asset_metadata_create(void)
{
AssetMetaData *asset_data = (AssetMetaData *)MEM_callocN(sizeof(*asset_data), __func__);
@ -115,6 +119,27 @@ void BKE_asset_library_reference_init_default(AssetLibraryReference *library_ref
memcpy(library_ref, DNA_struct_default_get(AssetLibraryReference), sizeof(*library_ref));
}
void BKE_asset_metadata_catalog_id_clear(struct AssetMetaData *asset_data)
{
asset_data->catalog_id = BLI_uuid_nil();
asset_data->catalog_simple_name[0] = '\0';
}
void BKE_asset_metadata_catalog_id_set(struct AssetMetaData *asset_data,
const bUUID catalog_id,
const char *catalog_simple_name)
{
asset_data->catalog_id = catalog_id;
constexpr size_t max_simple_name_length = sizeof(asset_data->catalog_simple_name);
/* The substr() call is necessary to make copy() copy the first N characters (instead of refusing
* to copy and producing an empty string). */
StringRef trimmed_id =
StringRef(catalog_simple_name).trim().substr(0, max_simple_name_length - 1);
trimmed_id.copy(asset_data->catalog_simple_name, max_simple_name_length);
}
/* Queries -------------------------------------------- */
PreviewImage *BKE_asset_metadata_preview_get_from_id(const AssetMetaData *UNUSED(asset_data),

View File

@ -0,0 +1,573 @@
/*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
/** \file
* \ingroup bke
*/
#include "BKE_asset_catalog.hh"
#include "BLI_fileops.h"
#include "BLI_path_util.h"
#include "BLI_string_ref.hh"
/* For S_ISREG() and S_ISDIR() on Windows. */
#ifdef WIN32
# include "BLI_winstuff.h"
#endif
#include <fstream>
namespace blender::bke {
const char AssetCatalogService::PATH_SEPARATOR = '/';
const CatalogFilePath AssetCatalogService::DEFAULT_CATALOG_FILENAME = "blender_assets.cats.txt";
AssetCatalogService::AssetCatalogService(const CatalogFilePath &asset_library_root)
: asset_library_root_(asset_library_root)
{
}
bool AssetCatalogService::is_empty() const
{
return catalogs_.is_empty();
}
AssetCatalog *AssetCatalogService::find_catalog(CatalogID catalog_id)
{
std::unique_ptr<AssetCatalog> *catalog_uptr_ptr = this->catalogs_.lookup_ptr(catalog_id);
if (catalog_uptr_ptr == nullptr) {
return nullptr;
}
return catalog_uptr_ptr->get();
}
void AssetCatalogService::delete_catalog(CatalogID catalog_id)
{
std::unique_ptr<AssetCatalog> *catalog_uptr_ptr = this->catalogs_.lookup_ptr(catalog_id);
if (catalog_uptr_ptr == nullptr) {
/* Catalog cannot be found, which is fine. */
return;
}
/* Mark the catalog as deleted. */
AssetCatalog *catalog = catalog_uptr_ptr->get();
catalog->flags.is_deleted = true;
/* Move ownership from this->catalogs_ to this->deleted_catalogs_. */
this->deleted_catalogs_.add(catalog_id, std::move(*catalog_uptr_ptr));
/* The catalog can now be removed from the map without freeing the actual AssetCatalog. */
this->catalogs_.remove(catalog_id);
this->rebuild_tree();
}
AssetCatalog *AssetCatalogService::create_catalog(const CatalogPath &catalog_path)
{
std::unique_ptr<AssetCatalog> catalog = AssetCatalog::from_path(catalog_path);
/* So we can std::move(catalog) and still use the non-owning pointer: */
AssetCatalog *const catalog_ptr = catalog.get();
/* TODO(@sybren): move the `AssetCatalog::from_path()` function to another place, that can reuse
* catalogs when a catalog with the given path is already known, and avoid duplicate catalog IDs.
*/
BLI_assert_msg(!catalogs_.contains(catalog->catalog_id), "duplicate catalog ID not supported");
catalogs_.add_new(catalog->catalog_id, std::move(catalog));
if (catalog_definition_file_) {
/* Ensure the new catalog gets written to disk at some point. If there is no CDF in memory yet,
* it's enough to have the catalog known to the service as it'll be saved to a new file. */
catalog_definition_file_->add_new(catalog_ptr);
}
return catalog_ptr;
}
static std::string asset_definition_default_file_path_from_dir(StringRef asset_library_root)
{
char file_path[PATH_MAX];
BLI_join_dirfile(file_path,
sizeof(file_path),
asset_library_root.data(),
AssetCatalogService::DEFAULT_CATALOG_FILENAME.data());
return file_path;
}
void AssetCatalogService::load_from_disk()
{
load_from_disk(asset_library_root_);
}
void AssetCatalogService::load_from_disk(const CatalogFilePath &file_or_directory_path)
{
BLI_stat_t status;
if (BLI_stat(file_or_directory_path.data(), &status) == -1) {
// TODO(@sybren): throw an appropriate exception.
return;
}
if (S_ISREG(status.st_mode)) {
load_single_file(file_or_directory_path);
}
else if (S_ISDIR(status.st_mode)) {
load_directory_recursive(file_or_directory_path);
}
else {
// TODO(@sybren): throw an appropriate exception.
}
/* TODO: Should there be a sanitize step? E.g. to remove catalogs with identical paths? */
catalog_tree_ = read_into_tree();
}
void AssetCatalogService::load_directory_recursive(const CatalogFilePath &directory_path)
{
// TODO(@sybren): implement proper multi-file support. For now, just load
// the default file if it is there.
CatalogFilePath file_path = asset_definition_default_file_path_from_dir(directory_path);
if (!BLI_exists(file_path.data())) {
/* No file to be loaded is perfectly fine. */
return;
}
this->load_single_file(file_path);
}
void AssetCatalogService::load_single_file(const CatalogFilePath &catalog_definition_file_path)
{
/* TODO(@sybren): check that #catalog_definition_file_path is contained in #asset_library_root_,
* otherwise some assumptions may fail. */
std::unique_ptr<AssetCatalogDefinitionFile> cdf = parse_catalog_file(
catalog_definition_file_path);
BLI_assert_msg(!this->catalog_definition_file_,
"Only loading of a single catalog definition file is supported.");
this->catalog_definition_file_ = std::move(cdf);
}
std::unique_ptr<AssetCatalogDefinitionFile> AssetCatalogService::parse_catalog_file(
const CatalogFilePath &catalog_definition_file_path)
{
auto cdf = std::make_unique<AssetCatalogDefinitionFile>();
cdf->file_path = catalog_definition_file_path;
auto catalog_parsed_callback = [this, catalog_definition_file_path](
std::unique_ptr<AssetCatalog> catalog) {
if (this->catalogs_.contains(catalog->catalog_id)) {
// TODO(@sybren): apparently another CDF was already loaded. This is not supported yet.
std::cerr << catalog_definition_file_path << ": multiple definitions of catalog "
<< catalog->catalog_id << " in multiple files, ignoring this one." << std::endl;
/* Don't store 'catalog'; unique_ptr will free its memory. */
return false;
}
/* The AssetCatalog pointer is now owned by the AssetCatalogService. */
this->catalogs_.add_new(catalog->catalog_id, std::move(catalog));
return true;
};
cdf->parse_catalog_file(cdf->file_path, catalog_parsed_callback);
return cdf;
}
void AssetCatalogService::merge_from_disk_before_writing()
{
/* TODO(Sybren): expand to support multiple CDFs. */
if (!catalog_definition_file_ || catalog_definition_file_->file_path.empty() ||
!BLI_is_file(catalog_definition_file_->file_path.c_str())) {
return;
}
auto catalog_parsed_callback = [this](std::unique_ptr<AssetCatalog> catalog) {
const bUUID catalog_id = catalog->catalog_id;
/* The following two conditions could be or'ed together. Keeping them separated helps when
* adding debug prints, breakpoints, etc. */
if (this->catalogs_.contains(catalog_id)) {
/* This catalog was already seen, so just ignore it. */
return false;
}
if (this->deleted_catalogs_.contains(catalog_id)) {
/* This catalog was already seen and subsequently deleted, so just ignore it. */
return false;
}
/* This is a new catalog, so let's keep it around. */
this->catalogs_.add_new(catalog_id, std::move(catalog));
return true;
};
catalog_definition_file_->parse_catalog_file(catalog_definition_file_->file_path,
catalog_parsed_callback);
}
bool AssetCatalogService::write_to_disk(const CatalogFilePath &directory_for_new_files)
{
/* TODO(Sybren): expand to support multiple CDFs. */
if (!catalog_definition_file_) {
if (catalogs_.is_empty() && deleted_catalogs_.is_empty()) {
/* Avoid saving anything, when there is nothing to save. */
return true; /* Writing nothing when there is nothing to write is still a success. */
}
/* A CDF has to be created to contain all current in-memory catalogs. */
const CatalogFilePath cdf_path = asset_definition_default_file_path_from_dir(
directory_for_new_files);
catalog_definition_file_ = construct_cdf_in_memory(cdf_path);
}
merge_from_disk_before_writing();
return catalog_definition_file_->write_to_disk();
}
std::unique_ptr<AssetCatalogDefinitionFile> AssetCatalogService::construct_cdf_in_memory(
const CatalogFilePath &file_path)
{
auto cdf = std::make_unique<AssetCatalogDefinitionFile>();
cdf->file_path = file_path;
for (auto &catalog : catalogs_.values()) {
cdf->add_new(catalog.get());
}
return cdf;
}
std::unique_ptr<AssetCatalogTree> AssetCatalogService::read_into_tree()
{
auto tree = std::make_unique<AssetCatalogTree>();
/* 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: Path should be formatted like a relative path");
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_;
}
}
return tree;
}
void AssetCatalogService::rebuild_tree()
{
this->catalog_tree_ = read_into_tree();
}
AssetCatalogTreeItem::AssetCatalogTreeItem(StringRef name, const AssetCatalogTreeItem *parent)
: name_(name), parent_(parent)
{
}
StringRef AssetCatalogTreeItem::get_name() const
{
return name_;
}
CatalogPath AssetCatalogTreeItem::catalog_path() const
{
std::string current_path = name_;
for (const AssetCatalogTreeItem *parent = parent_; parent; parent = parent->parent_) {
current_path = parent->name_ + AssetCatalogService::PATH_SEPARATOR + current_path;
}
return current_path;
}
int AssetCatalogTreeItem::count_parents() const
{
int i = 0;
for (const AssetCatalogTreeItem *parent = parent_; parent; parent = parent->parent_) {
i++;
}
return i;
}
void AssetCatalogTree::foreach_item(const AssetCatalogTreeItem::ItemIterFn callback) const
{
AssetCatalogTreeItem::foreach_item_recursive(children_, callback);
}
void AssetCatalogTreeItem::foreach_item_recursive(const AssetCatalogTreeItem::ChildMap &children,
const ItemIterFn callback)
{
for (const auto &[key, item] : children) {
callback(item);
foreach_item_recursive(item.children_, callback);
}
}
AssetCatalogTree *AssetCatalogService::get_catalog_tree()
{
return catalog_tree_.get();
}
bool AssetCatalogDefinitionFile::contains(const CatalogID catalog_id) const
{
return catalogs_.contains(catalog_id);
}
void AssetCatalogDefinitionFile::add_new(AssetCatalog *catalog)
{
catalogs_.add_new(catalog->catalog_id, catalog);
}
void AssetCatalogDefinitionFile::parse_catalog_file(
const CatalogFilePath &catalog_definition_file_path,
AssetCatalogParsedFn catalog_loaded_callback)
{
std::fstream infile(catalog_definition_file_path);
std::string line;
while (std::getline(infile, line)) {
const StringRef trimmed_line = StringRef(line).trim();
if (trimmed_line.is_empty() || trimmed_line[0] == '#') {
continue;
}
std::unique_ptr<AssetCatalog> catalog = this->parse_catalog_line(trimmed_line);
if (!catalog) {
continue;
}
AssetCatalog *non_owning_ptr = catalog.get();
const bool keep_catalog = catalog_loaded_callback(std::move(catalog));
if (!keep_catalog) {
continue;
}
if (this->contains(non_owning_ptr->catalog_id)) {
std::cerr << catalog_definition_file_path << ": multiple definitions of catalog "
<< non_owning_ptr->catalog_id << " in the same file, using first occurrence."
<< std::endl;
/* Don't store 'catalog'; unique_ptr will free its memory. */
continue;
}
/* The AssetDefinitionFile should include this catalog when writing it back to disk. */
this->add_new(non_owning_ptr);
}
}
std::unique_ptr<AssetCatalog> AssetCatalogDefinitionFile::parse_catalog_line(const StringRef line)
{
const char delim = ':';
const int64_t first_delim = line.find_first_of(delim);
if (first_delim == StringRef::not_found) {
std::cerr << "Invalid line in " << this->file_path << ": " << line << std::endl;
return std::unique_ptr<AssetCatalog>(nullptr);
}
/* Parse the catalog ID. */
const std::string id_as_string = line.substr(0, first_delim).trim();
bUUID catalog_id;
const bool uuid_parsed_ok = BLI_uuid_parse_string(&catalog_id, id_as_string.c_str());
if (!uuid_parsed_ok) {
std::cerr << "Invalid UUID in " << this->file_path << ": " << line << std::endl;
return std::unique_ptr<AssetCatalog>(nullptr);
}
/* Parse the path and simple name. */
const StringRef path_and_simple_name = line.substr(first_delim + 1);
const int64_t second_delim = path_and_simple_name.find_first_of(delim);
CatalogPath catalog_path;
std::string simple_name;
if (second_delim == 0) {
/* Delimiter as first character means there is no path. These lines are to be ignored. */
return std::unique_ptr<AssetCatalog>(nullptr);
}
if (second_delim == StringRef::not_found) {
/* No delimiter means no simple name, just treat it as all "path". */
catalog_path = path_and_simple_name;
simple_name = "";
}
else {
catalog_path = path_and_simple_name.substr(0, second_delim);
simple_name = path_and_simple_name.substr(second_delim + 1).trim();
}
catalog_path = AssetCatalog::cleanup_path(catalog_path);
return std::make_unique<AssetCatalog>(catalog_id, catalog_path, simple_name);
}
bool AssetCatalogDefinitionFile::write_to_disk() const
{
BLI_assert_msg(!this->file_path.empty(), "Writing to CDF requires its file path to be known");
return this->write_to_disk(this->file_path);
}
bool AssetCatalogDefinitionFile::write_to_disk(const CatalogFilePath &dest_file_path) const
{
const CatalogFilePath writable_path = dest_file_path + ".writing";
const CatalogFilePath backup_path = dest_file_path + "~";
if (!this->write_to_disk_unsafe(writable_path)) {
/* TODO: communicate what went wrong. */
return false;
}
if (BLI_exists(dest_file_path.c_str())) {
if (BLI_rename(dest_file_path.c_str(), backup_path.c_str())) {
/* TODO: communicate what went wrong. */
return false;
}
}
if (BLI_rename(writable_path.c_str(), dest_file_path.c_str())) {
/* TODO: communicate what went wrong. */
return false;
}
return true;
}
bool AssetCatalogDefinitionFile::write_to_disk_unsafe(const CatalogFilePath &dest_file_path) const
{
char directory[PATH_MAX];
BLI_split_dir_part(dest_file_path.c_str(), directory, sizeof(directory));
if (!ensure_directory_exists(directory)) {
/* TODO(Sybren): pass errors to the UI somehow. */
return false;
}
std::ofstream output(dest_file_path);
// TODO(@sybren): remember the line ending style that was originally read, then use that to write
// the file again.
// Write the header.
// TODO(@sybren): move the header definition to some other place.
output << "# This is an Asset Catalog Definition file for Blender." << std::endl;
output << "#" << std::endl;
output << "# Empty lines and lines starting with `#` will be ignored." << std::endl;
output << "# Other lines are of the format \"UUID:catalog/path/for/assets:simple catalog name\""
<< std::endl;
output << "" << std::endl;
// Write the catalogs.
// TODO(@sybren): order them by Catalog ID or Catalog Path.
for (const auto &catalog : catalogs_.values()) {
if (catalog->flags.is_deleted) {
continue;
}
output << catalog->catalog_id << ":" << catalog->path << ":" << catalog->simple_name
<< std::endl;
}
output.close();
return !output.bad();
}
bool AssetCatalogDefinitionFile::ensure_directory_exists(
const CatalogFilePath directory_path) const
{
/* TODO(@sybren): design a way to get such errors presented to users (or ensure that they never
* occur). */
if (directory_path.empty()) {
std::cerr
<< "AssetCatalogService: no asset library root configured, unable to ensure it exists."
<< std::endl;
return false;
}
if (BLI_exists(directory_path.data())) {
if (!BLI_is_dir(directory_path.data())) {
std::cerr << "AssetCatalogService: " << directory_path
<< " exists but is not a directory, this is not a supported situation."
<< std::endl;
return false;
}
/* Root directory exists, work is done. */
return true;
}
/* Ensure the root directory exists. */
std::error_code err_code;
if (!BLI_dir_create_recursive(directory_path.data())) {
std::cerr << "AssetCatalogService: error creating directory " << directory_path << ": "
<< err_code << std::endl;
return false;
}
/* Root directory has been created, work is done. */
return true;
}
AssetCatalog::AssetCatalog(const CatalogID catalog_id,
const CatalogPath &path,
const std::string &simple_name)
: catalog_id(catalog_id), path(path), simple_name(simple_name)
{
}
std::unique_ptr<AssetCatalog> AssetCatalog::from_path(const CatalogPath &path)
{
const CatalogPath clean_path = cleanup_path(path);
const CatalogID cat_id = BLI_uuid_generate_random();
const std::string simple_name = sensible_simple_name_for_path(clean_path);
auto catalog = std::make_unique<AssetCatalog>(cat_id, clean_path, simple_name);
return catalog;
}
std::string AssetCatalog::sensible_simple_name_for_path(const CatalogPath &path)
{
std::string name = path;
std::replace(name.begin(), name.end(), AssetCatalogService::PATH_SEPARATOR, '-');
if (name.length() < MAX_NAME - 1) {
return name;
}
/* Trim off the start of the path, as that's the most generic part and thus contains the least
* information. */
return "..." + name.substr(name.length() - 60);
}
CatalogPath AssetCatalog::cleanup_path(const CatalogPath &path)
{
/* TODO(@sybren): maybe go over each element of the path, and trim those? */
CatalogPath clean_path = StringRef(path).trim().trim(AssetCatalogService::PATH_SEPARATOR).trim();
return clean_path;
}
} // namespace blender::bke

View File

@ -0,0 +1,443 @@
/*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
* The Original Code is Copyright (C) 2020 Blender Foundation
* All rights reserved.
*/
#include "BKE_appdir.h"
#include "BKE_asset_catalog.hh"
#include "BLI_fileops.h"
#include "BLI_path_util.h"
#include "testing/testing.h"
namespace blender::bke::tests {
/* UUIDs from lib/tests/asset_library/blender_assets.cats.txt */
const bUUID UUID_ID_WITHOUT_PATH("e34dd2c5-5d2e-4668-9794-1db5de2a4f71");
const bUUID UUID_POSES_ELLIE("df60e1f6-2259-475b-93d9-69a1b4a8db78");
const bUUID UUID_POSES_ELLIE_WHITESPACE("b06132f6-5687-4751-a6dd-392740eb3c46");
const bUUID UUID_POSES_ELLIE_TRAILING_SLASH("3376b94b-a28d-4d05-86c1-bf30b937130d");
const bUUID UUID_POSES_RUZENA("79a4f887-ab60-4bd4-94da-d572e27d6aed");
const bUUID UUID_POSES_RUZENA_HAND("81811c31-1a88-4bd7-bb34-c6fc2607a12e");
const bUUID UUID_POSES_RUZENA_FACE("82162c1f-06cc-4d91-a9bf-4f72c104e348");
const bUUID UUID_WITHOUT_SIMPLENAME("d7916a31-6ca9-4909-955f-182ca2b81fa3");
/* UUIDs from lib/tests/asset_library/modified_assets.cats.txt */
const bUUID UUID_AGENT_47("c5744ba5-43f5-4f73-8e52-010ad4a61b34");
/* Subclass that adds accessors such that protected fields can be used in tests. */
class TestableAssetCatalogService : public AssetCatalogService {
public:
explicit TestableAssetCatalogService(const CatalogFilePath &asset_library_root)
: AssetCatalogService(asset_library_root)
{
}
AssetCatalogDefinitionFile *get_catalog_definition_file()
{
return catalog_definition_file_.get();
}
};
class AssetCatalogTest : public testing::Test {
protected:
CatalogFilePath asset_library_root_;
CatalogFilePath temp_library_path_;
void SetUp() override
{
const std::string test_files_dir = blender::tests::flags_test_asset_dir();
if (test_files_dir.empty()) {
FAIL();
}
asset_library_root_ = test_files_dir + "/" + "asset_library";
temp_library_path_ = "";
}
/* Register a temporary path, which will be removed at the end of the test.
* The returned path ends in a slash. */
CatalogFilePath use_temp_path()
{
BKE_tempdir_init("");
const CatalogFilePath tempdir = BKE_tempdir_session();
temp_library_path_ = tempdir + "test-temporary-path/";
return temp_library_path_;
}
CatalogFilePath create_temp_path()
{
CatalogFilePath path = use_temp_path();
BLI_dir_create_recursive(path.c_str());
return path;
}
struct CatalogPathInfo {
StringRef name;
int parent_count;
};
void assert_expected_tree_items(AssetCatalogTree *tree,
const std::vector<CatalogPathInfo> &expected_paths)
{
int i = 0;
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();
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());
i++;
});
}
void TearDown() override
{
if (!temp_library_path_.empty()) {
BLI_delete(temp_library_path_.c_str(), true, true);
temp_library_path_ = "";
}
}
};
TEST_F(AssetCatalogTest, load_single_file)
{
AssetCatalogService service(asset_library_root_);
service.load_from_disk(asset_library_root_ + "/" + "blender_assets.cats.txt");
// Test getting a non-existant catalog ID.
EXPECT_EQ(nullptr, service.find_catalog(BLI_uuid_generate_random()));
// Test getting an invalid catalog (without path definition).
AssetCatalog *cat_without_path = service.find_catalog(UUID_ID_WITHOUT_PATH);
ASSERT_EQ(nullptr, cat_without_path);
// Test getting a regular catalog.
AssetCatalog *poses_ellie = service.find_catalog(UUID_POSES_ELLIE);
ASSERT_NE(nullptr, poses_ellie);
EXPECT_EQ(UUID_POSES_ELLIE, poses_ellie->catalog_id);
EXPECT_EQ("character/Ellie/poselib", poses_ellie->path);
EXPECT_EQ("POSES_ELLIE", poses_ellie->simple_name);
// Test whitespace stripping and support in the path.
AssetCatalog *poses_whitespace = service.find_catalog(UUID_POSES_ELLIE_WHITESPACE);
ASSERT_NE(nullptr, poses_whitespace);
EXPECT_EQ(UUID_POSES_ELLIE_WHITESPACE, poses_whitespace->catalog_id);
EXPECT_EQ("character/Ellie/poselib/white space", poses_whitespace->path);
EXPECT_EQ("POSES_ELLIE WHITESPACE", poses_whitespace->simple_name);
// Test getting a UTF-8 catalog ID.
AssetCatalog *poses_ruzena = service.find_catalog(UUID_POSES_RUZENA);
ASSERT_NE(nullptr, poses_ruzena);
EXPECT_EQ(UUID_POSES_RUZENA, poses_ruzena->catalog_id);
EXPECT_EQ("character/Ružena/poselib", poses_ruzena->path);
EXPECT_EQ("POSES_RUŽENA", poses_ruzena->simple_name);
}
TEST_F(AssetCatalogTest, load_single_file_into_tree)
{
AssetCatalogService service(asset_library_root_);
service.load_from_disk(asset_library_root_ + "/" + "blender_assets.cats.txt");
/* Contains not only paths from the CDF but also the missing parents (implicitly defined
* catalogs). */
std::vector<CatalogPathInfo> expected_paths{
{"character", 0},
{"character/Ellie", 1},
{"character/Ellie/poselib", 2},
{"character/Ellie/poselib/white space", 3},
{"character/Ružena", 1},
{"character/Ružena/poselib", 2},
{"character/Ružena/poselib/face", 3},
{"character/Ružena/poselib/hand", 3},
{"path", 0}, // Implicit.
{"path/without", 1}, // Implicit.
{"path/without/simplename", 2}, // From CDF.
};
AssetCatalogTree *tree = service.get_catalog_tree();
assert_expected_tree_items(tree, expected_paths);
}
TEST_F(AssetCatalogTest, write_single_file)
{
TestableAssetCatalogService service(asset_library_root_);
service.load_from_disk(asset_library_root_ + "/" +
AssetCatalogService::DEFAULT_CATALOG_FILENAME);
const CatalogFilePath save_to_path = use_temp_path() +
AssetCatalogService::DEFAULT_CATALOG_FILENAME;
AssetCatalogDefinitionFile *cdf = service.get_catalog_definition_file();
cdf->write_to_disk(save_to_path);
AssetCatalogService loaded_service(save_to_path);
loaded_service.load_from_disk();
// Test that the expected catalogs are there.
EXPECT_NE(nullptr, loaded_service.find_catalog(UUID_POSES_ELLIE));
EXPECT_NE(nullptr, loaded_service.find_catalog(UUID_POSES_ELLIE_WHITESPACE));
EXPECT_NE(nullptr, loaded_service.find_catalog(UUID_POSES_ELLIE_TRAILING_SLASH));
EXPECT_NE(nullptr, loaded_service.find_catalog(UUID_POSES_RUZENA));
EXPECT_NE(nullptr, loaded_service.find_catalog(UUID_POSES_RUZENA_HAND));
EXPECT_NE(nullptr, loaded_service.find_catalog(UUID_POSES_RUZENA_FACE));
// Test that the invalid catalog definition wasn't copied.
EXPECT_EQ(nullptr, loaded_service.find_catalog(UUID_ID_WITHOUT_PATH));
// TODO(@sybren): test ordering of catalogs in the file.
}
TEST_F(AssetCatalogTest, no_writing_empty_files)
{
const CatalogFilePath temp_lib_root = create_temp_path();
AssetCatalogService service(temp_lib_root);
service.write_to_disk(temp_lib_root);
const CatalogFilePath default_cdf_path = temp_lib_root +
AssetCatalogService::DEFAULT_CATALOG_FILENAME;
EXPECT_FALSE(BLI_exists(default_cdf_path.c_str()));
}
TEST_F(AssetCatalogTest, create_first_catalog_from_scratch)
{
/* Even from scratch a root directory should be known. */
const CatalogFilePath temp_lib_root = use_temp_path();
AssetCatalogService service;
/* Just creating the service should NOT create the path. */
EXPECT_FALSE(BLI_exists(temp_lib_root.c_str()));
AssetCatalog *cat = service.create_catalog("some/catalog/path");
ASSERT_NE(nullptr, cat);
EXPECT_EQ(cat->path, "some/catalog/path");
EXPECT_EQ(cat->simple_name, "some-catalog-path");
/* Creating a new catalog should not save anything to disk yet. */
EXPECT_FALSE(BLI_exists(temp_lib_root.c_str()));
/* Writing to disk should create the directory + the default file. */
service.write_to_disk(temp_lib_root);
EXPECT_TRUE(BLI_is_dir(temp_lib_root.c_str()));
const CatalogFilePath definition_file_path = temp_lib_root + "/" +
AssetCatalogService::DEFAULT_CATALOG_FILENAME;
EXPECT_TRUE(BLI_is_file(definition_file_path.c_str()));
AssetCatalogService loaded_service(temp_lib_root);
loaded_service.load_from_disk();
// Test that the expected catalog is there.
AssetCatalog *written_cat = loaded_service.find_catalog(cat->catalog_id);
ASSERT_NE(nullptr, written_cat);
EXPECT_EQ(written_cat->catalog_id, cat->catalog_id);
EXPECT_EQ(written_cat->path, cat->path);
}
TEST_F(AssetCatalogTest, create_catalog_after_loading_file)
{
const CatalogFilePath temp_lib_root = create_temp_path();
/* Copy the asset catalog definition files to a separate location, so that we can test without
* overwriting the test file in SVN. */
const CatalogFilePath default_catalog_path = asset_library_root_ + "/" +
AssetCatalogService::DEFAULT_CATALOG_FILENAME;
const CatalogFilePath writable_catalog_path = temp_lib_root +
AssetCatalogService::DEFAULT_CATALOG_FILENAME;
BLI_copy(default_catalog_path.c_str(), writable_catalog_path.c_str());
EXPECT_TRUE(BLI_is_dir(temp_lib_root.c_str()));
EXPECT_TRUE(BLI_is_file(writable_catalog_path.c_str()));
TestableAssetCatalogService service(temp_lib_root);
service.load_from_disk();
EXPECT_EQ(writable_catalog_path, service.get_catalog_definition_file()->file_path);
EXPECT_NE(nullptr, service.find_catalog(UUID_POSES_ELLIE)) << "expected catalogs to be loaded";
/* This should create a new catalog but not write to disk. */
const AssetCatalog *new_catalog = service.create_catalog("new/catalog");
const bUUID new_catalog_id = new_catalog->catalog_id;
/* Reload the on-disk catalog file. */
TestableAssetCatalogService loaded_service(temp_lib_root);
loaded_service.load_from_disk();
EXPECT_EQ(writable_catalog_path, loaded_service.get_catalog_definition_file()->file_path);
EXPECT_NE(nullptr, loaded_service.find_catalog(UUID_POSES_ELLIE))
<< "expected pre-existing catalogs to be kept in the file";
EXPECT_EQ(nullptr, loaded_service.find_catalog(new_catalog_id))
<< "expecting newly added catalog to not yet be saved to " << temp_lib_root;
/* Write and reload the catalog file. */
service.write_to_disk(temp_lib_root.c_str());
AssetCatalogService reloaded_service(temp_lib_root);
reloaded_service.load_from_disk();
EXPECT_NE(nullptr, reloaded_service.find_catalog(UUID_POSES_ELLIE))
<< "expected pre-existing catalogs to be kept in the file";
EXPECT_NE(nullptr, reloaded_service.find_catalog(new_catalog_id))
<< "expecting newly added catalog to exist in the file";
}
TEST_F(AssetCatalogTest, create_catalog_path_cleanup)
{
const CatalogFilePath temp_lib_root = use_temp_path();
AssetCatalogService service(temp_lib_root);
AssetCatalog *cat = service.create_catalog(" /some/path / ");
EXPECT_FALSE(BLI_uuid_is_nil(cat->catalog_id));
EXPECT_EQ("some/path", cat->path);
EXPECT_EQ("some-path", cat->simple_name);
}
TEST_F(AssetCatalogTest, create_catalog_simple_name)
{
const CatalogFilePath temp_lib_root = use_temp_path();
AssetCatalogService service(temp_lib_root);
AssetCatalog *cat = service.create_catalog(
"production/Spite Fright/Characters/Victora/Pose Library/Approved/Body Parts/Hands");
EXPECT_FALSE(BLI_uuid_is_nil(cat->catalog_id));
EXPECT_EQ("production/Spite Fright/Characters/Victora/Pose Library/Approved/Body Parts/Hands",
cat->path);
EXPECT_EQ("...ht-Characters-Victora-Pose Library-Approved-Body Parts-Hands", cat->simple_name);
}
TEST_F(AssetCatalogTest, delete_catalog_leaf)
{
AssetCatalogService service(asset_library_root_);
service.load_from_disk(asset_library_root_ + "/" + "blender_assets.cats.txt");
/* Delete a leaf catalog, i.e. one that is not a parent of another catalog.
* This keeps this particular test easy. */
service.delete_catalog(UUID_POSES_RUZENA_HAND);
EXPECT_EQ(nullptr, service.find_catalog(UUID_POSES_RUZENA_HAND));
/* Contains not only paths from the CDF but also the missing parents (implicitly defined
* catalogs). This is why a leaf catalog was deleted. */
std::vector<CatalogPathInfo> expected_paths{
{"character", 0},
{"character/Ellie", 1},
{"character/Ellie/poselib", 2},
{"character/Ellie/poselib/white space", 3},
{"character/Ružena", 1},
{"character/Ružena/poselib", 2},
{"character/Ružena/poselib/face", 3},
// {"character/Ružena/poselib/hand", 3}, // this is the deleted one
{"path", 0},
{"path/without", 1},
{"path/without/simplename", 2},
};
AssetCatalogTree *tree = service.get_catalog_tree();
assert_expected_tree_items(tree, expected_paths);
}
TEST_F(AssetCatalogTest, delete_catalog_write_to_disk)
{
TestableAssetCatalogService service(asset_library_root_);
service.load_from_disk(asset_library_root_ + "/" +
AssetCatalogService::DEFAULT_CATALOG_FILENAME);
service.delete_catalog(UUID_POSES_ELLIE);
const CatalogFilePath save_to_path = use_temp_path();
AssetCatalogDefinitionFile *cdf = service.get_catalog_definition_file();
cdf->write_to_disk(save_to_path + "/" + AssetCatalogService::DEFAULT_CATALOG_FILENAME);
AssetCatalogService loaded_service(save_to_path);
loaded_service.load_from_disk();
// Test that the expected catalogs are there, except the deleted one.
EXPECT_EQ(nullptr, loaded_service.find_catalog(UUID_POSES_ELLIE));
EXPECT_NE(nullptr, loaded_service.find_catalog(UUID_POSES_ELLIE_WHITESPACE));
EXPECT_NE(nullptr, loaded_service.find_catalog(UUID_POSES_ELLIE_TRAILING_SLASH));
EXPECT_NE(nullptr, loaded_service.find_catalog(UUID_POSES_RUZENA));
EXPECT_NE(nullptr, loaded_service.find_catalog(UUID_POSES_RUZENA_HAND));
EXPECT_NE(nullptr, loaded_service.find_catalog(UUID_POSES_RUZENA_FACE));
}
TEST_F(AssetCatalogTest, merge_catalog_files)
{
const CatalogFilePath cdf_dir = create_temp_path();
const CatalogFilePath original_cdf_file = asset_library_root_ + "/blender_assets.cats.txt";
const CatalogFilePath modified_cdf_file = asset_library_root_ + "/modified_assets.cats.txt";
const CatalogFilePath temp_cdf_file = cdf_dir + "blender_assets.cats.txt";
BLI_copy(original_cdf_file.c_str(), temp_cdf_file.c_str());
// Load the unmodified, original CDF.
TestableAssetCatalogService service(asset_library_root_);
service.load_from_disk(cdf_dir);
// Copy a modified file, to mimick a situation where someone changed the CDF after we loaded it.
BLI_copy(modified_cdf_file.c_str(), temp_cdf_file.c_str());
// Overwrite the modified file. This should merge the on-disk file with our catalogs.
service.write_to_disk(cdf_dir);
AssetCatalogService loaded_service(cdf_dir);
loaded_service.load_from_disk();
// Test that the expected catalogs are there.
EXPECT_NE(nullptr, loaded_service.find_catalog(UUID_POSES_ELLIE));
EXPECT_NE(nullptr, loaded_service.find_catalog(UUID_POSES_ELLIE_WHITESPACE));
EXPECT_NE(nullptr, loaded_service.find_catalog(UUID_POSES_ELLIE_TRAILING_SLASH));
EXPECT_NE(nullptr, loaded_service.find_catalog(UUID_POSES_RUZENA));
EXPECT_NE(nullptr, loaded_service.find_catalog(UUID_POSES_RUZENA_HAND));
EXPECT_NE(nullptr, loaded_service.find_catalog(UUID_POSES_RUZENA_FACE));
EXPECT_NE(nullptr, loaded_service.find_catalog(UUID_AGENT_47)); // New in the modified file.
// When there are overlaps, the in-memory (i.e. last-saved) paths should win.
const AssetCatalog *ruzena_face = loaded_service.find_catalog(UUID_POSES_RUZENA_FACE);
EXPECT_EQ("character/Ružena/poselib/face", ruzena_face->path);
}
TEST_F(AssetCatalogTest, backups)
{
const CatalogFilePath cdf_dir = create_temp_path();
const CatalogFilePath original_cdf_file = asset_library_root_ + "/blender_assets.cats.txt";
const CatalogFilePath writable_cdf_file = cdf_dir + "/blender_assets.cats.txt";
BLI_copy(original_cdf_file.c_str(), writable_cdf_file.c_str());
/* Read a CDF, modify, and write it. */
AssetCatalogService service(cdf_dir);
service.load_from_disk();
service.delete_catalog(UUID_POSES_ELLIE);
service.write_to_disk(cdf_dir);
const CatalogFilePath backup_path = writable_cdf_file + "~";
ASSERT_TRUE(BLI_is_file(backup_path.c_str()));
AssetCatalogService loaded_service;
loaded_service.load_from_disk(backup_path);
// Test that the expected catalogs are there, including the deleted one.
// This is the backup, after all.
EXPECT_NE(nullptr, loaded_service.find_catalog(UUID_POSES_ELLIE));
EXPECT_NE(nullptr, loaded_service.find_catalog(UUID_POSES_ELLIE_WHITESPACE));
EXPECT_NE(nullptr, loaded_service.find_catalog(UUID_POSES_ELLIE_TRAILING_SLASH));
EXPECT_NE(nullptr, loaded_service.find_catalog(UUID_POSES_RUZENA));
EXPECT_NE(nullptr, loaded_service.find_catalog(UUID_POSES_RUZENA_HAND));
EXPECT_NE(nullptr, loaded_service.find_catalog(UUID_POSES_RUZENA_FACE));
}
} // namespace blender::bke::tests

View File

@ -0,0 +1,53 @@
/*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
/** \file
* \ingroup bke
*/
#include "BKE_asset_library.hh"
#include "MEM_guardedalloc.h"
#include <memory>
/**
* Loading an asset library at this point only means loading the catalogs. Later on this should
* invoke reading of asset representations too.
*/
struct AssetLibrary *BKE_asset_library_load(const char *library_path)
{
blender::bke::AssetLibrary *lib = new blender::bke::AssetLibrary();
lib->load(library_path);
return reinterpret_cast<struct AssetLibrary *>(lib);
}
void BKE_asset_library_free(struct AssetLibrary *asset_library)
{
blender::bke::AssetLibrary *lib = reinterpret_cast<blender::bke::AssetLibrary *>(asset_library);
delete lib;
}
namespace blender::bke {
void AssetLibrary::load(StringRefNull library_root_directory)
{
auto catalog_service = std::make_unique<AssetCatalogService>(library_root_directory);
catalog_service->load_from_disk();
this->catalog_service = std::move(catalog_service);
}
} // namespace blender::bke

View File

@ -0,0 +1,82 @@
/*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
* The Original Code is Copyright (C) 2020 Blender Foundation
* All rights reserved.
*/
#include "BKE_appdir.h"
#include "BKE_asset_catalog.hh"
#include "BKE_asset_library.hh"
#include "testing/testing.h"
namespace blender::bke::tests {
TEST(AssetLibraryTest, load_and_free_c_functions)
{
const std::string test_files_dir = blender::tests::flags_test_asset_dir();
if (test_files_dir.empty()) {
FAIL();
}
/* Load the asset library. */
const std::string library_path = test_files_dir + "/" + "asset_library";
::AssetLibrary *library_c_ptr = BKE_asset_library_load(library_path.data());
ASSERT_NE(nullptr, library_c_ptr);
/* Check that it can be cast to the C++ type and has a Catalog Service. */
blender::bke::AssetLibrary *library_cpp_ptr = reinterpret_cast<blender::bke::AssetLibrary *>(
library_c_ptr);
AssetCatalogService *service = library_cpp_ptr->catalog_service.get();
ASSERT_NE(nullptr, service);
/* Check that the catalogs defined in the library are actually loaded. This just tests one single
* catalog, as that indicates the file has been loaded. Testing that that loading went OK is for
* the asset catalog service tests. */
const bUUID uuid_poses_ellie("df60e1f6-2259-475b-93d9-69a1b4a8db78");
AssetCatalog *poses_ellie = service->find_catalog(uuid_poses_ellie);
ASSERT_NE(nullptr, poses_ellie) << "unable to find POSES_ELLIE catalog";
EXPECT_EQ("character/Ellie/poselib", poses_ellie->path);
BKE_asset_library_free(library_c_ptr);
}
TEST(AssetLibraryTest, load_nonexistent_directory)
{
const std::string test_files_dir = blender::tests::flags_test_asset_dir();
if (test_files_dir.empty()) {
FAIL();
}
/* Load the asset library. */
const std::string library_path = test_files_dir + "/" +
"asset_library/this/subdir/does/not/exist";
::AssetLibrary *library_c_ptr = BKE_asset_library_load(library_path.data());
ASSERT_NE(nullptr, library_c_ptr);
/* Check that it can be cast to the C++ type and has a Catalog Service. */
blender::bke::AssetLibrary *library_cpp_ptr = reinterpret_cast<blender::bke::AssetLibrary *>(
library_c_ptr);
AssetCatalogService *service = library_cpp_ptr->catalog_service.get();
ASSERT_NE(nullptr, service);
/* Check that the catalog service doesn't have any catalogs. */
EXPECT_TRUE(service->is_empty());
BKE_asset_library_free(library_c_ptr);
}
} // namespace blender::bke::tests

View File

@ -0,0 +1,70 @@
/*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
* The Original Code is Copyright (C) 2020 Blender Foundation
* All rights reserved.
*/
#include "BKE_asset.h"
#include "BLI_uuid.h"
#include "DNA_asset_types.h"
#include "testing/testing.h"
namespace blender::bke::tests {
TEST(AssetMetadataTest, set_catalog_id)
{
AssetMetaData meta;
const bUUID uuid = BLI_uuid_generate_random();
/* Test trivial values. */
BKE_asset_metadata_catalog_id_clear(&meta);
EXPECT_TRUE(BLI_uuid_is_nil(meta.catalog_id));
EXPECT_STREQ("", meta.catalog_simple_name);
/* Test simple situation where the given short name is used as-is. */
BKE_asset_metadata_catalog_id_set(&meta, uuid, "simple");
EXPECT_TRUE(BLI_uuid_equal(uuid, meta.catalog_id));
EXPECT_STREQ("simple", meta.catalog_simple_name);
/* Test whitespace trimming. */
BKE_asset_metadata_catalog_id_set(&meta, uuid, " Govoriš angleško? ");
EXPECT_STREQ("Govoriš angleško?", meta.catalog_simple_name);
/* Test length trimming to 63 chars + terminating zero. */
constexpr char len66[] = "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20";
constexpr char len63[] = "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1";
BKE_asset_metadata_catalog_id_set(&meta, uuid, len66);
EXPECT_STREQ(len63, meta.catalog_simple_name);
/* Test length trimming happens after whitespace trimming. */
constexpr char len68[] =
" \
000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20 ";
BKE_asset_metadata_catalog_id_set(&meta, uuid, len68);
EXPECT_STREQ(len63, meta.catalog_simple_name);
/* Test length trimming to 63 bytes, and not 63 characters. ✓ in UTF-8 is three bytes long. */
constexpr char with_utf8[] =
"00010203040506✓0708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20";
BKE_asset_metadata_catalog_id_set(&meta, uuid, with_utf8);
EXPECT_STREQ("00010203040506✓0708090a0b0c0d0e0f101112131415161718191a1b1c1d",
meta.catalog_simple_name);
}
} // namespace blender::bke::tests

View File

@ -73,4 +73,19 @@ bool BLI_uuid_parse_string(bUUID *uuid, const char *buffer) ATTR_NONNULL();
/** Output the UUID as formatted ASCII string, see #BLI_uuid_format(). */
std::ostream &operator<<(std::ostream &stream, bUUID uuid);
namespace blender::bke {
class bUUID : public ::bUUID {
public:
bUUID() = default;
bUUID(const ::bUUID &struct_uuid);
explicit bUUID(const std::string &string_formatted_uuid);
uint64_t hash() const;
};
bool operator==(bUUID uuid1, bUUID uuid2);
} // namespace blender::bke
#endif

View File

@ -24,6 +24,7 @@
#include <cstring>
#include <ctime>
#include <random>
#include <sstream>
#include <string>
/* Ensure the UUID struct doesn't have any padding, to be compatible with memcmp(). */
@ -137,3 +138,34 @@ std::ostream &operator<<(std::ostream &stream, bUUID uuid)
stream << buffer;
return stream;
}
namespace blender::bke {
bUUID::bUUID(const std::string &string_formatted_uuid)
{
const bool parsed_ok = BLI_uuid_parse_string(this, string_formatted_uuid.c_str());
if (!parsed_ok) {
std::stringstream ss;
ss << "invalid UUID string " << string_formatted_uuid;
throw std::runtime_error(ss.str());
}
}
bUUID::bUUID(const ::bUUID &struct_uuid)
{
*(static_cast<::bUUID *>(this)) = struct_uuid;
}
uint64_t bUUID::hash() const
{
/* Convert the struct into two 64-bit numbers, and XOR them to get the hash. */
const uint64_t *uuid_as_int64 = reinterpret_cast<const uint64_t *>(this);
return uuid_as_int64[0] ^ uuid_as_int64[1];
}
bool operator==(bUUID uuid1, bUUID uuid2)
{
return BLI_uuid_equal(uuid1, uuid2);
}
} // namespace blender::bke

View File

@ -49,6 +49,7 @@ set(SRC
)
set(LIB
bf_blenkernel
)
if(WITH_HEADLESS)

View File

@ -56,6 +56,7 @@
#endif
#include "BKE_asset.h"
#include "BKE_asset_library.h"
#include "BKE_context.h"
#include "BKE_global.h"
#include "BKE_icons.h"
@ -386,6 +387,7 @@ typedef struct FileList {
eFileSelectType type;
/* The library this list was created for. Stored here so we know when to re-read. */
AssetLibraryReference *asset_library_ref;
struct AssetLibrary *asset_library;
short flags;
@ -1758,6 +1760,13 @@ void filelist_clear_ex(struct FileList *filelist, const bool do_cache, const boo
if (do_selection && filelist->selection_state) {
BLI_ghash_clear(filelist->selection_state, NULL, NULL);
}
if (filelist->asset_library != NULL) {
/* There is no way to refresh the catalogs stored by the AssetLibrary struct, so instead of
* "clearing" it, the entire struct is freed. It will be reallocated when needed. */
BKE_asset_library_free(filelist->asset_library);
filelist->asset_library = NULL;
}
}
void filelist_clear(struct FileList *filelist)
@ -3136,6 +3145,9 @@ typedef struct FileListReadJob {
* The job system calls #filelist_readjob_update which moves any read file from #tmp_filelist
* into #filelist in a thread-safe way.
*
* #tmp_filelist also keeps an `AssetLibrary *` so that it can be loaded in the same thread, and
* moved to #filelist once all categories are loaded.
*
* NOTE: #tmp_filelist is freed in #filelist_readjob_free, so any copied pointers need to be set
* to NULL to avoid double-freeing them. */
struct FileList *tmp_filelist;
@ -3266,6 +3278,13 @@ static void filelist_readjob_do(const bool do_lib,
BLI_stack_discard(todo_dirs);
}
BLI_stack_free(todo_dirs);
/* Check whether assets catalogs need to be loaded. */
if (job_params->filelist->asset_library_ref != NULL) {
/* Load asset catalogs, into the temp filelist for thread-safety.
* #filelist_readjob_endjob() will move it into the real filelist. */
job_params->tmp_filelist->asset_library = BKE_asset_library_load(filelist->filelist.root);
}
}
static void filelist_readjob_dir(FileListReadJob *job_params,
@ -3355,6 +3374,8 @@ static void filelist_readjob_startjob(void *flrjv, short *stop, short *do_update
BLI_mutex_lock(&flrj->lock);
BLI_assert((flrj->tmp_filelist == NULL) && flrj->filelist);
BLI_assert_msg(flrj->filelist->asset_library == NULL,
"Asset library should not yet be assigned at start of read job");
flrj->tmp_filelist = MEM_dupallocN(flrj->filelist);
@ -3415,6 +3436,12 @@ static void filelist_readjob_endjob(void *flrjv)
/* In case there would be some dangling update... */
filelist_readjob_update(flrjv);
/* Move ownership of the asset library from the temporary list to the true filelist. */
BLI_assert_msg(flrj->filelist->asset_library == NULL,
"asset library should not already have been allocated");
flrj->filelist->asset_library = flrj->tmp_filelist->asset_library;
flrj->tmp_filelist->asset_library = NULL; /* MUST be NULL to avoid double-free. */
flrj->filelist->flags &= ~FL_IS_PENDING;
flrj->filelist->flags |= FL_IS_READY;
}

View File

@ -22,6 +22,7 @@
#include "DNA_defs.h"
#include "DNA_listBase.h"
#include "DNA_uuid_types.h"
#ifdef __cplusplus
extern "C" {
@ -58,6 +59,19 @@ typedef struct AssetMetaData {
/** Custom asset meta-data. Cannot store pointers to IDs (#STRUCT_NO_DATABLOCK_IDPROPERTIES)! */
struct IDProperty *properties;
/**
* Asset Catalog identifier. Should not contain spaces.
* Mapped to a path in the asset catalog hierarchy by an #AssetCatalogService.
* Use #BKE_asset_metadata_catalog_id_set() to ensure a valid ID is set.
*/
struct bUUID catalog_id;
/**
* Short name of the asset's catalog. This is for debugging purposes only, to allow (partial)
* reconstruction of asset catalogs in the unfortunate case that the mapping from catalog UUID to
* catalog path is lost. The catalog's simple name is copied to #catalog_simple_name whenever
* #catalog_id is updated. */
char catalog_simple_name[64]; /* MAX_NAME */
/** Optional description of this asset for display in the UI. Dynamic length. */
char *description;
/** User defined tags for this asset. The asset manager uses these for filtering, but how they

View File

@ -40,6 +40,12 @@ typedef struct bUUID {
uint8_t node[6];
} bUUID;
/**
* Memory required for a string representation of a UUID according to RFC4122.
* This is 36 characters for the string + a trailing zero byte.
*/
#define UUID_STRING_LEN 37
#ifdef __cplusplus
}
#endif

View File

@ -32,11 +32,15 @@
#ifdef RNA_RUNTIME
# include "BKE_asset.h"
# include "BKE_asset_library.h"
# include "BKE_context.h"
# include "BKE_idprop.h"
# include "BLI_listbase.h"
# include "BLI_uuid.h"
# include "ED_asset.h"
# include "ED_fileselect.h"
# include "RNA_access.h"
@ -176,6 +180,40 @@ static void rna_AssetMetaData_active_tag_range(
*max = *softmax = MAX2(asset_data->tot_tags - 1, 0);
}
static void rna_AssetMetaData_catalog_id_get(PointerRNA *ptr, char *value)
{
const AssetMetaData *asset_data = ptr->data;
BLI_uuid_format(value, asset_data->catalog_id);
}
static int rna_AssetMetaData_catalog_id_length(PointerRNA *UNUSED(ptr))
{
return UUID_STRING_LEN - 1;
}
static void rna_AssetMetaData_catalog_id_set(PointerRNA *ptr, const char *value)
{
AssetMetaData *asset_data = ptr->data;
bUUID new_uuid;
if (value[0] == '\0') {
BKE_asset_metadata_catalog_id_clear(asset_data);
return;
}
if (!BLI_uuid_parse_string(&new_uuid, value)) {
// TODO(Sybren): raise ValueError exception once that's possible from an RNA setter.
printf("UUID %s not formatted correctly, ignoring new value\n", value);
return;
}
/* This just sets the new UUID and clears the catalog simple name. The actual
* catalog simple name will be updated by some update function, as it
* needs the asset library from the context. */
/* TODO(Sybren): write that update function. */
BKE_asset_metadata_catalog_id_set(asset_data, new_uuid, "");
}
static PointerRNA rna_AssetHandle_file_data_get(PointerRNA *ptr)
{
AssetHandle *asset_handle = ptr->data;
@ -310,6 +348,24 @@ static void rna_def_asset_data(BlenderRNA *brna)
prop = RNA_def_property(srna, "active_tag", PROP_INT, PROP_NONE);
RNA_def_property_int_funcs(prop, NULL, NULL, "rna_AssetMetaData_active_tag_range");
RNA_def_property_ui_text(prop, "Active Tag", "Index of the tag set for editing");
prop = RNA_def_property(srna, "catalog_id", PROP_STRING, PROP_NONE);
RNA_def_property_string_funcs(prop,
"rna_AssetMetaData_catalog_id_get",
"rna_AssetMetaData_catalog_id_length",
"rna_AssetMetaData_catalog_id_set");
RNA_def_property_flag(prop, PROP_CONTEXT_UPDATE);
RNA_def_property_ui_text(prop,
"Catalog UUID",
"Identifier for the asset's catalog, used by Blender to look up the "
"asset's catalog path. Must be a UUID according to RFC4122");
prop = RNA_def_property(srna, "catalog_simple_name", PROP_STRING, PROP_NONE);
RNA_def_property_clear_flag(prop, PROP_EDITABLE);
RNA_def_property_ui_text(prop,
"Catalog Simple Name",
"Simple name of the asset's catalog, for debugging and "
"data recovery purposes");
}
static void rna_def_asset_handle_api(StructRNA *srna)