diff options
19 files changed, 1755 insertions, 0 deletions
diff --git a/release/scripts/startup/bl_ui/space_filebrowser.py b/release/scripts/startup/bl_ui/space_filebrowser.py index e52136fc416..ee9a8cc3346 100644 --- a/release/scripts/startup/bl_ui/space_filebrowser.py +++ b/release/scripts/startup/bl_ui/space_filebrowser.py @@ -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() diff --git a/source/blender/blenkernel/BKE_asset.h b/source/blender/blenkernel/BKE_asset.h index 50eb2859279..7ae5378272d 100644 --- a/source/blender/blenkernel/BKE_asset.h +++ b/source/blender/blenkernel/BKE_asset.h @@ -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, diff --git a/source/blender/blenkernel/BKE_asset_catalog.hh b/source/blender/blenkernel/BKE_asset_catalog.hh new file mode 100644 index 00000000000..2bbaa4b4222 --- /dev/null +++ b/source/blender/blenkernel/BKE_asset_catalog.hh @@ -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 diff --git a/source/blender/blenkernel/BKE_asset_library.h b/source/blender/blenkernel/BKE_asset_library.h new file mode 100644 index 00000000000..709b915f9ff --- /dev/null +++ b/source/blender/blenkernel/BKE_asset_library.h @@ -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 diff --git a/source/blender/blenkernel/BKE_asset_library.hh b/source/blender/blenkernel/BKE_asset_library.hh new file mode 100644 index 00000000000..68f7481574e --- /dev/null +++ b/source/blender/blenkernel/BKE_asset_library.hh @@ -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 diff --git a/source/blender/blenkernel/CMakeLists.txt b/source/blender/blenkernel/CMakeLists.txt index de7864ef36a..d04d4558fed 100644 --- a/source/blender/blenkernel/CMakeLists.txt +++ b/source/blender/blenkernel/CMakeLists.txt @@ -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 diff --git a/source/blender/blenkernel/intern/asset.cc b/source/blender/blenkernel/intern/asset.cc index f74018b20c5..ac9bcccc8bd 100644 --- a/source/blender/blenkernel/intern/asset.cc +++ b/source/blender/blenkernel/intern/asset.cc @@ -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), diff --git a/source/blender/blenkernel/intern/asset_catalog.cc b/source/blender/blenkernel/intern/asset_catalog.cc new file mode 100644 index 00000000000..0c64a0b085c --- /dev/null +++ b/source/blender/blenkernel/intern/asset_catalog.cc @@ -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 diff --git a/source/blender/blenkernel/intern/asset_catalog_test.cc b/source/blender/blenkernel/intern/asset_catalog_test.cc new file mode 100644 index 00000000000..0f389999d6d --- /dev/null +++ b/source/blender/blenkernel/intern/asset_catalog_test.cc @@ -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 diff --git a/source/blender/blenkernel/intern/asset_library.cc b/source/blender/blenkernel/intern/asset_library.cc new file mode 100644 index 00000000000..1153e7b29f5 --- /dev/null +++ b/source/blender/blenkernel/intern/asset_library.cc @@ -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 diff --git a/source/blender/blenkernel/intern/asset_library_test.cc b/source/blender/blenkernel/intern/asset_library_test.cc new file mode 100644 index 00000000000..37686175aed --- /dev/null +++ b/source/blender/blenkernel/intern/asset_library_test.cc @@ -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 diff --git a/source/blender/blenkernel/intern/asset_test.cc b/source/blender/blenkernel/intern/asset_test.cc new file mode 100644 index 00000000000..f1bf21a4f83 --- /dev/null +++ b/source/blender/blenkernel/intern/asset_test.cc @@ -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 diff --git a/source/blender/blenlib/BLI_uuid.h b/source/blender/blenlib/BLI_uuid.h index 9b85f8e65bc..592ac3d4607 100644 --- a/source/blender/blenlib/BLI_uuid.h +++ b/source/blender/blenlib/BLI_uuid.h @@ -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 diff --git a/source/blender/blenlib/intern/uuid.cc b/source/blender/blenlib/intern/uuid.cc index ae34bcb3d32..c1f7f8dfecd 100644 --- a/source/blender/blenlib/intern/uuid.cc +++ b/source/blender/blenlib/intern/uuid.cc @@ -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 diff --git a/source/blender/editors/space_file/CMakeLists.txt b/source/blender/editors/space_file/CMakeLists.txt index 993a52b9084..b60f9df82f6 100644 --- a/source/blender/editors/space_file/CMakeLists.txt +++ b/source/blender/editors/space_file/CMakeLists.txt @@ -49,6 +49,7 @@ set(SRC ) set(LIB + bf_blenkernel ) if(WITH_HEADLESS) diff --git a/source/blender/editors/space_file/filelist.c b/source/blender/editors/space_file/filelist.c index 511b5b255e9..4f881184990 100644 --- a/source/blender/editors/space_file/filelist.c +++ b/source/blender/editors/space_file/filelist.c @@ -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; } diff --git a/source/blender/makesdna/DNA_asset_types.h b/source/blender/makesdna/DNA_asset_types.h index 2975915eccd..f5bdad3e79e 100644 --- a/source/blender/makesdna/DNA_asset_types.h +++ b/source/blender/makesdna/DNA_asset_types.h @@ -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 diff --git a/source/blender/makesdna/DNA_uuid_types.h b/source/blender/makesdna/DNA_uuid_types.h index fa0a78f074b..dcebfed6be7 100644 --- a/source/blender/makesdna/DNA_uuid_types.h +++ b/source/blender/makesdna/DNA_uuid_types.h @@ -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 diff --git a/source/blender/makesrna/intern/rna_asset.c b/source/blender/makesrna/intern/rna_asset.c index dcef88d2e79..5df32150cc4 100644 --- a/source/blender/makesrna/intern/rna_asset.c +++ b/source/blender/makesrna/intern/rna_asset.c @@ -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) |