Add Chromium-only Blender WebEngine parity work

This commit is contained in:
mes123456
2026-08-12 04:47:48 -04:00
commit 9fd26010f6
18225 changed files with 11622124 additions and 0 deletions

View File

@@ -0,0 +1,399 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup asset_system
*/
#pragma once
#include <memory>
#include <mutex>
#include <set>
#include <string>
#include "BLI_function_ref.hh"
#include "BLI_map.hh"
#include "BLI_set.hh"
#include "BLI_uuid.h"
#include "BLI_vector.hh"
#include "AS_asset_catalog_path.hh"
namespace blender::asset_system {
class AssetCatalog;
class AssetCatalogCollection;
class AssetCatalogDefinitionFile;
class AssetCatalogFilter;
class AssetCatalogTree;
using CatalogID = UUID;
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;
using OwningAssetCatalogMap = Map<CatalogID, std::unique_ptr<AssetCatalog>>;
/* Manages the asset catalogs of a single asset library (i.e. of catalogs defined in a single
* directory hierarchy). */
class AssetCatalogService {
std::unique_ptr<AssetCatalogCollection> catalog_collection_;
/**
* Cached catalog tree storage. Lazy-created by #AssetCatalogService::catalog_tree().
*/
std::shared_ptr<AssetCatalogTree> catalog_tree_;
std::recursive_mutex catalog_tree_mutex_;
Vector<std::unique_ptr<AssetCatalogCollection>> undo_snapshots_;
Vector<std::unique_ptr<AssetCatalogCollection>> redo_snapshots_;
CatalogFilePath asset_library_root_;
bool is_read_only_ = false;
friend class AssetLibraryService;
friend class AssetLibrary;
public:
static const CatalogFilePath DEFAULT_CATALOG_FILENAME;
struct read_only_tag {};
explicit AssetCatalogService(const CatalogFilePath &asset_library_root = {},
std::optional<read_only_tag> read_only_tag = std::nullopt);
explicit AssetCatalogService(read_only_tag);
~AssetCatalogService();
/**
* Set tag indicating that some catalog modifications are unsaved, which could
* get lost on exit. This tag is not set by internal catalog code, the catalog
* service user is responsible for it. It is cleared by #write_to_disk().
*
* This "dirty" state is tracked per catalog, so that it's possible to gracefully load changes
* from disk. Any catalog with unsaved changes will not be overwritten by on-disk changes. */
void tag_has_unsaved_changes(AssetCatalog *edited_catalog = nullptr);
bool has_unsaved_changes() const;
/**
* Check if this is a read-only service meaning the user shouldn't be able to do edits. This is
* not enforced by internal catalog code, the catalog service user is responsible for it. For
* example the UI should disallow edits.
*/
bool is_read_only() const;
/** 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);
/**
* Duplicate the catalogs from \a other_service into this one. Does not rebuild the tree, this
* needs to be done by the caller (call #rebuild_tree()!).
*
* \note If a catalog from \a other already exists in this collection (identified by catalog ID),
* it will be skipped and \a on_duplicate_items will be called.
*/
void add_from_existing(const AssetCatalogService &other_service,
FunctionRef<void(const AssetCatalog &existing,
const AssetCatalog &to_be_ignored)> on_duplicate_items);
/**
* Write the catalog definitions to disk.
*
* The location where the catalogs are saved is variable, and depends on the location of the
* blend file. The first matching rule wins:
*
* - Already loaded a CDF from disk?
* -> Always write to that file.
* - The directory containing the blend file has a blender_assets.cats.txt file?
* -> Merge with & write to that file.
* - The directory containing the blend file is part of an asset library, as per
* the user's preferences?
* -> Merge with & write to ${ASSET_LIBRARY_ROOT}/blender_assets.cats.txt
* - Create a new file blender_assets.cats.txt next to the blend file.
*
* Return true on success, which either means there were no in-memory categories to save,
* or the save was successful. */
bool write_to_disk(const CatalogFilePath &blend_file_path);
/**
* Ensure that the next call to #on_blend_save_post() will choose a new location for the CDF
* suitable for the location of the blend file (regardless of where the current catalogs come
* from), and that catalogs will be merged with already-existing ones in that location.
*
* Use this for a "Save as..." that has to write the catalogs to the new blend file location,
* instead of updating the previously read CDF. */
void prepare_to_merge_on_write();
/**
* 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 reload_catalogs();
/** Return catalog with the given ID. Return nullptr if not found. */
AssetCatalog *find_catalog(CatalogID catalog_id) const;
/**
* Return first catalog with the given path. Return nullptr if not found. This is not an
* efficient call as it's just a linear search over the catalogs.
*
* If there are multiple catalogs with the same path, return the first-loaded one. If there is
* none marked as "first loaded", return the one with the lowest UUID. */
AssetCatalog *find_catalog_by_path(const AssetCatalogPath &path) const;
/**
* Return true only if this catalog is known.
* This treats deleted catalogs as "unknown". */
bool is_catalog_known(CatalogID catalog_id) const;
/**
* Create a filter object that can be used to determine whether an asset belongs to the given
* catalog, or any of the catalogs in the sub-tree rooted at the given catalog.
*
* \see #AssetCatalogFilter
*/
AssetCatalogFilter create_catalog_filter(CatalogID active_catalog_id) const;
/**
* Create a catalog with some sensible auto-generated catalog ID.
* The catalog will be saved to the default catalog file.
*
* NOTE: this does NOT mark the catalog service itself as 'has changes'. The caller is
* responsible for that.
*
* \see #tag_has_unsaved_changes()
*/
AssetCatalog *create_catalog(const AssetCatalogPath &catalog_path);
/**
* Delete all catalogs with the given path, and their children.
*/
void prune_catalogs_by_path(const AssetCatalogPath &path);
/**
* Delete all catalogs with the same path as the identified catalog, and their children.
* This call is the same as calling `prune_catalogs_by_path(find_catalog(catalog_id)->path)`.
*/
void prune_catalogs_by_id(CatalogID catalog_id);
/**
* Update the catalog path, also updating the catalog path of all sub-catalogs.
*/
void update_catalog_path(CatalogID catalog_id, const AssetCatalogPath &new_catalog_path);
/**
* May be called from multiple threads.
*/
std::shared_ptr<const AssetCatalogTree> catalog_tree();
/** Return true only if there are no catalogs known. */
bool is_empty() const;
/**
* Store the current catalogs in the undo stack.
* This snapshots everything in the #AssetCatalogCollection. */
void undo_push();
/**
* Restore the last-saved undo snapshot, pushing the current state onto the redo stack.
* The caller is responsible for first checking that undoing is possible.
*/
void undo();
bool is_undo_possbile() const;
/**
* Restore the last-saved redo snapshot, pushing the current state onto the undo stack.
* The caller is responsible for first checking that undoing is possible. */
void redo();
bool is_redo_possbile() const;
protected:
void load_directory_recursive(const CatalogFilePath &directory_path);
void load_single_file(const CatalogFilePath &catalog_definition_file_path);
/** Implementation of #write_to_disk() that doesn't clear the "has unsaved changes" tag. */
bool write_to_disk_ex(const CatalogFilePath &blend_file_path);
void untag_has_unsaved_changes();
bool is_catalog_known_with_unsaved_changes(CatalogID catalog_id) const;
/**
* Delete catalogs, only keeping them when they are either listed in
* \a catalogs_to_keep or have unsaved changes.
*
* \note Deleted catalogs are hard-deleted, i.e. they just vanish instead of
* remembering them as "deleted".
*/
void purge_catalogs_not_listed(const Set<CatalogID> &catalogs_to_keep);
/**
* Delete a catalog, without deleting any of its children and without rebuilding the catalog
* tree. The deletion in "Soft", in the sense that the catalog pointer is moved from `catalogs_`
* to `deleted_catalogs_`; the AssetCatalog instance itself is kept in memory. As a result, it
* will be removed from a CDF when saved to disk.
*
* This is a lower-level function than #prune_catalogs_by_path.
*
* NOTE: this does NOT mark the catalog service itself as 'has changes'. The caller is
* responsible for that.
*
* \see #tag_has_unsaved_changes()
*/
void delete_catalog_by_id_soft(CatalogID catalog_id);
/**
* Hard delete a catalog. This simply removes the catalog from existence. The deletion will not
* be remembered, and reloading the CDF will bring it back. */
void delete_catalog_by_id_hard(CatalogID catalog_id);
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) const;
/**
* Find a suitable path to write a CDF to.
*
* This depends on the location of the blend file, and on whether a CDF already exists next to it
* or whether the blend file is saved inside an asset library.
*/
static CatalogFilePath find_suitable_cdf_path_for_writing(
const CatalogFilePath &blend_file_path);
std::unique_ptr<AssetCatalogTree> read_into_tree() const;
/**
* Ensure a #catalog_tree() will update the tree. Must be called whenever the contained user
* visible catalogs change.
* May be called from multiple threads.
*/
void invalidate_catalog_tree();
/**
* For every catalog, ensure that its parent path also has a known catalog.
*/
void create_missing_catalogs();
/**
* For every catalog, mark it as "dirty".
*/
void tag_all_catalogs_as_unsaved_changes();
/* For access by subclasses, as those will not be marked as friend by #AssetCatalogCollection. */
const AssetCatalogDefinitionFile *get_catalog_definition_file() const;
const OwningAssetCatalogMap &get_catalogs() const;
const OwningAssetCatalogMap &get_deleted_catalogs() const;
};
/**
* Asset Catalog definition, containing a symbolic ID and a path that points to a node in the
* catalog hierarchy.
*
* \warning The asset system may reload catalogs, invalidating pointers. Thus it's not recommended
* to store pointers to asset catalogs. Store the #CatalogID instead and do a lookup when
* needed.
*/
class AssetCatalog {
public:
const CatalogID catalog_id;
AssetCatalogPath 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 data-loss when the catalog definition file gets lost,
* we also store a human-readable simple name for the catalog.
*
* It should fit in sizeof(AssetMetaData::catalog_simple_name) bytes. */
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;
/* Sort this catalog first when there are multiple catalogs with the same catalog path. This
* ensures that in a situation where missing catalogs were auto-created, and then
* load-and-merged with a file that also has these catalogs, the first one in that file is
* always sorted first, regardless of the sort order of its UUID. */
bool is_first_loaded = false;
/* Merging on-disk changes into memory will not overwrite this catalog.
* For example, when a catalog was renamed (i.e. changed path) in this Blender session,
* reloading the catalog definition file should not overwrite that change.
*
* Note that this flag is ignored when is_deleted=true; deleted catalogs that are still in
* memory are considered "unsaved" by definition. */
bool has_unsaved_changes = false;
} flags;
AssetCatalog() = delete;
AssetCatalog(CatalogID catalog_id, const AssetCatalogPath &path, const std::string &simple_name);
/**
* Create a new Catalog with the given path, auto-generating a sensible catalog simple-name.
*
* 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 AssetCatalogPath &path);
/** Make a new simple name for the catalog, based on its path. */
void simple_name_refresh();
protected:
/** Generate a sensible catalog ID for the given path. */
static std::string sensible_simple_name_for_path(const AssetCatalogPath &path);
};
/** Comparator for asset catalogs, ordering by (path, first_seen, UUID). */
struct AssetCatalogLessThan {
bool operator()(const AssetCatalog *lhs, const AssetCatalog *rhs) const
{
if (lhs->path != rhs->path) {
return lhs->path < rhs->path;
}
if (lhs->flags.is_first_loaded != rhs->flags.is_first_loaded) {
return lhs->flags.is_first_loaded;
}
return lhs->catalog_id < rhs->catalog_id;
}
};
/**
* Set that stores catalogs ordered by (path, UUID).
* Being a set, duplicates are removed. The catalog's simple name is ignored in this. */
using AssetCatalogOrderedSet = std::set<const AssetCatalog *, AssetCatalogLessThan>;
using MutableAssetCatalogOrderedSet = std::set<AssetCatalog *, AssetCatalogLessThan>;
/**
* Filter that can determine whether an asset should be visible or not, based on its catalog ID.
*
* \see AssetCatalogService::create_catalog_filter()
*/
class AssetCatalogFilter {
const Set<CatalogID> matching_catalog_ids_;
const Set<CatalogID> known_catalog_ids_;
friend AssetCatalogService;
public:
bool contains(CatalogID asset_catalog_id) const;
/* So that all unknown catalogs can be shown under "Unassigned". */
bool is_known(CatalogID asset_catalog_id) const;
protected:
explicit AssetCatalogFilter(Set<CatalogID> &&matching_catalog_ids,
Set<CatalogID> &&known_catalog_ids);
};
} // namespace blender::asset_system

View File

@@ -0,0 +1,131 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup asset_system
*/
#pragma once
#include "BLI_function_ref.hh"
#include "BLI_string_ref.hh"
#include <string>
namespace blender::asset_system {
/**
* Location of an Asset Catalog in the catalog tree, denoted by slash-separated path components.
*
* Each path component is a string that is not allowed to have slashes or colons. The latter is to
* make things easy to save in the colon-delimited Catalog Definition File format.
*
* The path of a catalog determines where in the catalog hierarchy the catalog is shown. Examples
* are "Characters/Ellie/Poses/Hand" or "Kit_bash/City/Skyscrapers". The path looks like a
* file-system path, with a few differences:
*
* - Only slashes are used as path component separators.
* - All paths are absolute, so there is no need for a leading slash.
*
* See https://developer.blender.org/docs/features/asset_system/backend/asset_catalogs/
*
* Paths are stored as byte sequences, and assumed to be UTF8.
*/
class AssetCatalogPath {
friend std::ostream &operator<<(std::ostream &stream, const AssetCatalogPath &path_to_append);
/**
* The path itself, such as "Agents/Secret/327".
*/
std::string path_;
public:
static const char SEPARATOR;
AssetCatalogPath() = default;
AssetCatalogPath(StringRef path);
AssetCatalogPath(std::string path);
AssetCatalogPath(const char *path);
AssetCatalogPath(const AssetCatalogPath &other_path) = default;
AssetCatalogPath(AssetCatalogPath &&other_path) noexcept;
~AssetCatalogPath() = default;
uint64_t hash() const;
uint64_t length() const; /* Length of the path in bytes. */
/** C-string representation of the path. */
const char *c_str() const;
const std::string &str() const;
/* The last path component, used as label in the tree view. */
StringRefNull name() const;
/* In-class operators, because of the implicit `AssetCatalogPath(StringRef)` constructor.
* Otherwise `string == string` could cast both sides to `AssetCatalogPath`. */
bool operator==(const AssetCatalogPath &other_path) const;
bool operator!=(const AssetCatalogPath &other_path) const;
bool operator<(const AssetCatalogPath &other_path) const;
AssetCatalogPath &operator=(const AssetCatalogPath &other_path) = default;
AssetCatalogPath &operator=(AssetCatalogPath &&other_path) = default;
/** Concatenate two paths, returning the new path. */
AssetCatalogPath operator/(const AssetCatalogPath &path_to_append) const;
/* False when the path is empty, true otherwise. */
operator bool() const;
/** Creates and ensures that the path is cleaned up. */
static AssetCatalogPath from_user_input(const char *path);
/**
* Clean up the path. This ensures:
* - Every path component is stripped of its leading/trailing spaces.
* - Empty components (caused by double slashes or leading/trailing slashes) are removed.
* - Invalid characters are replaced with valid ones.
*/
[[nodiscard]] AssetCatalogPath cleanup() const;
/**
* \return true only if the given path is a parent of this catalog's path.
* When this catalog's path is equal to the given path, return true as well.
* In other words, this defines a weak subset.
*
* True: "some/path/there" is contained in "some/path" and "some".
* False: "path/there" is not contained in "some/path/there".
*
* Note that non-cleaned-up paths (so for example starting or ending with a
* slash) are not supported, and result in undefined behavior.
*/
bool is_contained_in(const AssetCatalogPath &other_path) const;
/**
* \return the parent path, or an empty path if there is no parent.
*/
AssetCatalogPath parent() const;
/**
* Change the initial part of the path from `from_path` to `to_path`.
* If this path does not start with `from_path`, return an empty path as result.
*
* Example:
*
* AssetCatalogPath path("some/path/to/some/catalog");
* path.rebase("some/path", "new/base") -> "new/base/to/some/catalog"
*/
AssetCatalogPath rebase(const AssetCatalogPath &from_path,
const AssetCatalogPath &to_path) const;
/** Call the callback function for each path component, in left-to-right order. */
using ComponentIteratorFn = FunctionRef<void(StringRef component_name, bool is_last_component)>;
void iterate_components(ComponentIteratorFn callback) const;
protected:
/** Strip leading/trailing spaces and replace disallowed characters. */
static std::string cleanup_component(StringRef component_name);
};
/** Output the path as string. */
std::ostream &operator<<(std::ostream &stream, const AssetCatalogPath &path_to_append);
} // namespace blender::asset_system

View File

@@ -0,0 +1,109 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup asset_system
*
* A representation of the catalog paths as tree structure. Each component of the catalog tree is
* represented by an #AssetCatalogTreeItem. The last path component of an item is used as its name,
* which may also be shown to the user.
* An item can not have multiple children with the same name. That means the name uniquely
* identifies an item within its parent.
*
* There is no single root tree element, the #AssetCatalogTree instance itself represents the root.
*/
#pragma once
#include <map>
#include <optional>
#include "AS_asset_catalog.hh"
namespace blender::asset_system {
/**
* Representation of a catalog path in the #AssetCatalogTree.
*/
class AssetCatalogTreeItem {
public:
/** Container for child items. Uses a #std::map to keep items ordered by their name (i.e. their
* last catalog component). */
using ChildMap = std::map<std::string, AssetCatalogTreeItem>;
using ItemIterFn = FunctionRef<void(const AssetCatalogTreeItem &)>;
private:
/** Child tree items, ordered by their names. */
ChildMap children_;
/** The user visible name of this component. */
CatalogPathComponent name_;
CatalogID catalog_id_;
/** Copy of #AssetCatalog::simple_name. */
std::string simple_name_;
/** Copy of #AssetCatalog::flags.has_unsaved_changes. */
bool has_unsaved_changes_ = false;
/** Pointer back to the parent item. Used to reconstruct the hierarchy from an item (e.g. to
* build a path). */
const AssetCatalogTreeItem *parent_ = nullptr;
friend class AssetCatalogTree;
public:
AssetCatalogTreeItem(StringRef name,
CatalogID catalog_id,
StringRef simple_name,
const AssetCatalogTreeItem *parent = nullptr);
CatalogID get_catalog_id() const;
StringRefNull get_simple_name() const;
StringRefNull get_name() const;
bool has_unsaved_changes() 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. */
AssetCatalogPath catalog_path() const;
int count_parents() const;
bool has_children() const;
/** Iterate over children calling \a callback for each of them, but do not recurse into their
* children. */
void foreach_child(ItemIterFn callback) const;
void foreach_item(ItemIterFn callback) const;
private:
static void foreach_item_recursive(const ChildMap &children_, ItemIterFn callback);
};
class AssetCatalogTree {
using ChildMap = AssetCatalogTreeItem::ChildMap;
using ItemIterFn = AssetCatalogTreeItem::ItemIterFn;
/** Child tree items, ordered by their names. */
ChildMap root_items_;
public:
/**
* Ensure an item representing \a catalog is in the tree, adding it if necessary.
*
* \param skip_prefix: If set and the catalog path starts with this prefix path, the prefix path
* will be stripped, and the catalog will be inserted into the tree as if it started after
* this prefix. For example if the path of \a catalog is "Lorem ipsum/dolor/sit", and \a
* skip_prefix is set to "Lorem ipsum/dolor", then the catalog will be inserted as if the path
* was "sit". Catalogs whose path do not start with the prefix will be unaffected.
*/
void insert_item(const AssetCatalog &catalog,
std::optional<StringRef> skip_prefix = std::nullopt);
void foreach_item(ItemIterFn callback) const;
/** Iterate over root items calling \a callback for each of them, but do not recurse into their
* children. */
void foreach_root_item(ItemIterFn callback) const;
bool is_empty() const;
const AssetCatalogTreeItem *find_item(const AssetCatalogPath &path) const;
const AssetCatalogTreeItem *find_root_item(const AssetCatalogPath &path) const;
};
} // namespace blender::asset_system

View File

@@ -0,0 +1,30 @@
/* SPDX-FileCopyrightText: 2026 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup asset_system
*/
#pragma once
namespace blender::asset_system {
/**
* Status of the asset's file(s) on disk, compared to the remote asset listing.
*/
enum class RemoteAssetFileStatus {
/** Just so you can recognize a zero-initialized field of this type. */
UNSET = 0,
/** The asset's main file does not exist on disk. */
NOT_ON_DISK = 1,
/** All the asset's files exist on disk, and match the listing's hashes. */
MATCH = 2,
/** At least one of the asset's files exists on disk, but doesn't match the listing's hash. */
NO_MATCH = 3,
/* In the future there will likely be another option here: INCOMPLETE. It will indicate that the
* asset's main file, which contains the asset datablock, exists, but the asset's other files do
* not. As such, this will only be added when Blender supports multi-file assets. */
};
} // namespace blender::asset_system

View File

@@ -0,0 +1,401 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup asset_system
*/
#pragma once
#include <memory>
#include <mutex>
#include <optional>
#include "AS_asset_catalog.hh"
#include "AS_asset_representation.hh" /* For URLWithHash. */
#include "DNA_asset_types.h"
#include "BLI_mutex.hh"
#include "BLI_set.hh"
#include "BLI_string_ref.hh"
#include "BLI_vector.hh"
#include "BKE_callbacks.hh"
namespace blender {
struct Main;
namespace bke::id {
class IDRemapper;
}
namespace asset_system {
class AssetRepresentation;
/**
* AssetLibrary provides access to an asset library's data.
*
* The asset library contains catalogs and storage for asset representations. It could be extended
* to also include asset indexes and more.
*/
class AssetLibrary {
eAssetLibraryType library_type_;
/** See #is_read_only(). */
bool is_read_only_ = true;
/**
* The name this asset library will be displayed as in the UI. Will also be used as a weak way
* to identify an asset library (e.g. by #AssetWeakReference).
*/
std::string name_;
/** If this is an asset library on disk, the top-level directory path. Normalized using
* #normalize_directory_path(). Shared pointer so assets can safely point to it, and don't have
* to hold a copy (which is the size of `std::string` + the allocated buffer, if no short string
* optimization is used). With thousands of assets this might make a reasonable difference. */
std::shared_ptr<std::string> root_path_;
/**
* AssetStorage for assets (better said their representations) that are considered to be part of
* this library. Assets are not automatically loaded into this when loading an asset library.
* Assets have to be loaded externally and added to this storage via
* #add_external_on_disk_asset() or #add_local_id_asset(). So this really is arbitrary storage as
* far as #AssetLibrary is concerned (allowing the API user to manage partial library storage and
* partial loading, so only relevant parts of a library are kept in memory).
*
* For now, multiple parts of Blender just keep adding their own assets to this storage. E.g.
* multiple asset browsers might load multiple representations for the same asset into this.
* Currently there is just no way to properly identify assets, or keep track of which assets are
* already in memory and which not. Neither do we keep track of how many parts of Blender are
* using an asset or an asset library, which is needed to know when assets can be freed.
*/
struct AssetStorage {
/* Uses shared pointers so the UI can acquire weak pointers. It can then ensure pointers are
* not dangling before accessing. */
Set<std::shared_ptr<AssetRepresentation>> external_assets;
Mutex external_assets_mutex;
/* Store local ID assets separately for efficient lookups.
* TODO(Julian): A [ID *, asset] or even [ID.session_uid, asset] map would be preferable for
* faster lookups. Not possible until each asset is only represented once in the storage. */
Set<std::shared_ptr<AssetRepresentation>> local_id_assets;
Mutex local_id_assets_mutex;
};
AssetStorage asset_storage_;
protected:
/* Changing this pointer should be protected using #catalog_service_mutex_. Note that changes
* within the catalog service may still happen without the mutex being locked. They should be
* protected separately.
*
* This is a #shared_ptr (rather than #unique_ptr) so that readers can keep the service alive
* while using it, even if another thread replaces #catalog_service_ in the meantime (which frees
* the previously referenced service). See #catalog_service_ptr(). */
std::shared_ptr<AssetCatalogService> catalog_service_;
mutable std::recursive_mutex catalog_service_mutex_;
/** Assets owned by this library may be imported with a different method than set in
* #import_method_ above, it's just a default. */
bool may_override_import_method_ = false;
bCallbackFuncStore on_save_callback_store_{};
public:
/* Controlled by #ed::asset::catalogs_set_save_catalogs_when_file_is_saved,
* for managing the "Save Catalog Changes" in the quit-confirmation dialog box. */
static bool save_catalogs_when_file_is_saved;
friend class AssetLibraryService;
friend class AssetRepresentation;
/**
* \param is_read_only: If true, the user should not be able to edit assets or asset catalogs
* from this library. See #is_read_only().
* \param name: The name this asset library will be displayed in the UI as. Will also be used as
* a weak way to identify an asset library (e.g. by #AssetWeakReference). Make sure
* this is set for any custom (not builtin) asset library. That is,
* #ASSET_LIBRARY_CUSTOM ones.
* \param root_path: If this is an asset library on disk, the top-level directory path.
*/
AssetLibrary(eAssetLibraryType library_type,
bool is_read_only,
StringRef name = "",
StringRef root_path = "");
virtual ~AssetLibrary();
/**
* Execute \a fn for every asset library that is loaded and enabled. The asset library is passed
* to the \a fn call.
*
* \note Libraries may note be freed during the iteration.
*
* \param include_all_library: When true, \a fn will also be executed for the "All" asset
* library. This is just a combination of the other ones, so usually iterating over it is
* redundant.
*/
static void foreach_loaded(FunctionRef<void(AssetLibrary &)> fn, bool include_all_library);
/**
* (Re-)download the remote listing for this library.
*
* This only has an effect for asset libraries that are themselves a remote library, or contain
* one (such as the "Essentials" library if it includes online essentials, or the "All" if there
* are any remote libraries included).
*
* The "Allow Online Access" option will be enforced internally, but probably some check to give
* a user message should be done at a higher levl.
*/
virtual void force_remote_listing_download() const;
/**
* Get the #AssetLibraryReference referencing this library. This can fail for custom libraries,
* which have too look up their #bUserAssetLibrary. It will not return a value for values that
* were loaded directly through a path.
*/
virtual std::optional<AssetLibraryReference> library_reference() const = 0;
/**
* Get the import method that should be used for assets in this library.
*
* \return The import method or no value if the library doesn't support importing. For example
* because the library is the "Current File" library or the library was removed from the
* Preferences.
*/
virtual std::optional<eAssetImportMethod> import_method() const = 0;
virtual bool use_relative_paths() const;
/**
* Return the URL of the remote asset library, or #std::nullopt if this is not a remote library.
*
* Note: don't use this as a way to distinguish remote vs. local libraries. Either query the
* asset itself, or use #is_or_contains_remote_libraries(). The Essentials and All libraries may
* contain a mixture of remote and local assets.
*/
virtual std::optional<StringRefNull> remote_url() const;
AssetCatalogService &catalog_service() const;
/**
* Get shared ownership of the catalog service. Unlike #catalog_service(), this keeps the service
* alive for as long as the returned pointer is held, even if another thread replaces the
* library's catalog service in the meantime (e.g. a background catalog reload job). Use this
* instead of #catalog_service() when accessing the service from a thread that may run
* concurrently with such a replacement (e.g. the drawing/main thread while an asset read job is
* running). */
std::shared_ptr<AssetCatalogService> catalog_service_ptr() const;
/**
* Create a representation of an asset to be considered part of this library. Once the
* representation is not needed anymore, it must be freed using #remove_asset(), or there will be
* leaking that's only cleared when the library storage is destructed (typically on exit or
* loading a different file).
*
* \param relative_asset_path: The path of the asset relative to the asset library root. With
* this the asset must be uniquely identifiable within the asset
* library.
* \return A weak pointer to the new asset representation. The caller needs to keep some
* reference stored to be able to call #remove_asset(). This would be dangling once the
* asset library is destructed, so a weak pointer should be used to reference it.
*/
std::weak_ptr<AssetRepresentation> add_external_on_disk_asset(
StringRef relative_asset_path,
StringRef name,
int id_type,
std::unique_ptr<AssetMetaData> metadata);
/** See #AssetLibrary::add_external_on_disk_asset(). Use this for assets that are not available
* on disk, and part of an online asset library. */
std::weak_ptr<AssetRepresentation> add_external_online_asset(
StringRef relative_asset_path,
StringRef name,
int id_type,
std::unique_ptr<AssetMetaData> metadata,
OnlineAssetInfo online_info);
/** See #AssetLibrary::add_external_on_disk_asset(). */
std::weak_ptr<AssetRepresentation> add_local_id_asset(ID &id);
/**
* Remove an asset from the library that was added using #add_external_on_disk_asset() or
* #add_local_id_asset(). Can usually be expected to be constant time complexity (worst case may
* differ).
* \note This is safe to call if \a asset is freed (dangling reference), will not perform any
* change then.
* \return True on success, false if the asset couldn't be found inside the library (also the
* case when the reference is dangling).
*/
bool remove_asset(AssetRepresentation &asset);
/**
* Remap ID pointers for local ID assets, see #BKE_lib_remap.hh. When an ID pointer would be
* mapped to null (typically when an ID gets removed), the asset is removed, because we don't
* support such empty/null assets.
*/
void remap_ids_and_remove_invalid(const bke::id::IDRemapper &mappings);
/**
* Update `catalog_simple_name` by looking up the asset's catalog by its ID.
*
* No-op if the catalog cannot be found. This could be the kind of "the
* catalog definition file is corrupt/lost" scenario that the simple name is
* meant to help recover from.
*/
void refresh_catalog_simplename(AssetMetaData *asset_data);
void load_or_reload_catalogs();
void on_blend_save_handler_register();
void on_blend_save_handler_unregister();
void on_blend_save_post(Main *bmain, PointerRNA **pointers, int num_pointers);
std::string resolve_asset_weak_reference_to_full_path(const AssetWeakReference &asset_reference);
eAssetLibraryType library_type() const;
StringRefNull name() const;
StringRefNull root_path() const;
/**
* Check if this is a read-only library, meaning the user shouldn't be able to do edits to
* assets and asset catalogs from this library.
*
* \note This isn't enforced by the asset system - the UI or other editing code has to respect
* this flag. Also see #AssetCatalogService::is_read_only().
*
* Of course it's possible to modify the .blend files containing the assets manually; and
* similarly, to open a .blend file in the library directory to edit asset catalogs. This
* function only speaks for editing directly *via this library*.
*/
bool is_read_only() const;
protected:
/** Load catalogs that have changed on disk. */
virtual void refresh_catalogs();
};
/** Get all asset library references which are enabled and for which the directory exists. */
Vector<AssetLibraryReference> all_valid_asset_library_refs();
AssetLibraryReference all_library_reference();
AssetLibraryReference essentials_library_reference();
AssetLibraryReference current_file_library_reference();
AssetLibraryReference online_essentials_library_reference();
void all_library_tag_catalogs_dirty();
void all_library_reload_catalogs_if_dirty();
/**
* Return whether this is a remote asset library, or contains remote assets.
*
* The All and Essentials libraries can have a mixture of local & remote assets.
*/
bool is_or_contains_remote_libraries(const AssetLibraryReference &reference);
bool contains_assets_from_remote_url(const AssetLibrary &library, StringRef remote_url);
} // namespace asset_system
/**
* Load the data for an asset library, but not the asset representations themselves (loading these
* is currently not done in the asset system).
*
* For the "All" asset library (#ASSET_LIBRARY_ALL), every other known asset library will be
* loaded as well. So a call to #AssetLibrary::foreach_loaded() can be expected to iterate over all
* libraries.
*
* \warning Catalogs are reloaded, invalidating catalog pointers. Do not store catalog pointers,
* store CatalogIDs instead and lookup the catalog where needed.
*/
asset_system::AssetLibrary *AS_asset_library_load(const Main *bmain,
const AssetLibraryReference &library_reference);
std::string AS_asset_library_root_path_from_library_ref(
const AssetLibraryReference &library_reference);
/**
* Try to find an appropriate location for an asset library root from a file or directory path.
* Does not check if \a input_path exists.
*
* The design is made to find an appropriate asset library path from a .blend file path, but
* technically works with any file or directory as \a input_path.
* Design is:
* * If \a input_path lies within a known asset library path (i.e. an asset library registered in
* the Preferences), return the asset library path.
* * Otherwise, if \a input_path has a parent path, return the parent path (e.g. to use the
* directory a .blend file is in as asset library root).
* * If \a input_path is empty or doesn't have a parent path (e.g. because a .blend wasn't saved
* yet), there is no suitable path. The caller has to decide how to handle this case.
*
* \return The returned asset library path with a trailing slash,
* or an empty string if no suitable path is found.
*/
std::string AS_asset_library_find_suitable_root_path_from_path(StringRefNull input_path);
/**
* Uses the current location on disk of the file represented by \a bmain as input to
* #AS_asset_library_find_suitable_root_path_from_path(). Refer to it for a design
* description.
*
* \return True if the function could find a valid, that is, a non-empty path to return in \a
* r_library_path. If \a bmain wasn't saved into a file yet, the return value will be
* false.
*/
std::string AS_asset_library_find_suitable_root_path_from_main(const Main *bmain);
/**
* Force clearing of all asset library data. After calling this, new asset libraries can be loaded
* just as usual using #AS_asset_library_load(), no init or other setup is needed.
*
* Does not need to be called on exit, this is handled internally.
*/
void AS_asset_libraries_exit();
/**
* Return the #AssetLibrary rooted at the given directory path.
*
* Will return the same pointer for repeated calls, until another blend file is loaded.
*
* To get the in-memory-only "current file" asset library, pass an empty path.
*/
asset_system::AssetLibrary *AS_asset_library_load_from_directory(const char *name,
const char *library_dirpath);
/** Return whether any loaded AssetLibrary has unsaved changes to its catalogs. */
bool AS_asset_library_has_any_unsaved_catalogs();
/**
* An asset library can include local IDs (IDs in the current file). Their pointers need to be
* remapped on change (or assets removed as IDs gets removed).
*/
void AS_asset_library_remap_ids(const bke::id::IDRemapper &mappings);
/**
* Attempt to resolve a full path to an asset based on the currently available (not necessary
* loaded) asset libraries, and split it into it's directory, ID group and ID name components. The
* path is not guaranteed to exist on disk. On failure to resolve the reference, return arguments
* will point to null.
*
* \note Only works for asset libraries on disk and the "Current File" one (others can't be
* resolved).
*
* \param r_path_buffer: Buffer to hold the result in on success. Will be the full path with null
* terminators instead of slashes separating the directory, group and name
* components. Must be at least #FILE_MAX_LIBEXTRA long.
* \param r_dir: Returns the .blend file path with native slashes on success. Optional (passing
* null is allowed). For the "Current File" library this will be empty.
* \param r_group: Returns the ID group such as "Object", "Material" or "Brush". Optional (passing
* null is allowed).
* \param r_name: Returns the ID name on success. Optional (passing null is allowed).
*/
void AS_asset_full_path_explode_from_weak_ref(const AssetWeakReference *asset_reference,
char r_path_buffer[/*FILE_MAX_LIBEXTRA*/ 1282],
char **r_dir,
char **r_group,
char **r_name);
/**
* Updates the default import method for asset libraries based on
* #U.experimental.no_data_block_packing.
*/
void AS_asset_library_import_method_ensure_valid(Main &bmain);
} // namespace blender

View File

@@ -0,0 +1,266 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup asset_system
*
* \brief Main runtime representation of an asset.
*
* Abstraction to reference an asset, with necessary data for display & interaction.
* https://developer.blender.org/docs/features/asset_system/backend/#asset-representation
*/
#pragma once
#include <memory>
#include <optional>
#include <string>
#include <variant>
#include "BLI_string_ref.hh"
#include "BLI_utility_mixins.hh"
#include "DNA_ID_enums.h"
#include "DNA_asset_types.h"
#include "AS_asset_file_status.hh"
namespace blender {
struct AssetMetaData;
struct bContext;
struct ID;
struct PreviewImage;
struct ReportList;
namespace asset_system {
class AssetLibrary;
struct OnlineAssetInfo;
struct OnlineAssetFile;
struct URLWithHash;
class AssetRepresentation : NonCopyable, NonMovable {
/** Pointer back to the asset library that owns this asset representation. */
AssetLibrary &owner_asset_library_;
/**
* Uniquely identifies the asset within the asset library. Currently this is always a path (path
* within the asset library).
*/
/* Mutable to allow lazy updating on name changes in #library_relative_identifier(). */
mutable std::string relative_identifier_;
struct ExternalAsset {
std::string name;
int id_type = 0;
std::unique_ptr<AssetMetaData> metadata_ = nullptr;
PreviewImage *preview_ = nullptr;
/**
* Status of this asset's file(s) compared to the remote listing.
* Only meaningful for assets from a remote library that have been checked against the listing.
* For online-only assets (#online_info_ is set), the status is stored there instead.
*
* \see #AssetRepresentation::remote_file_status()
* \see #AssetRepresentation::remote_file_status_set()
*/
RemoteAssetFileStatus remote_file_status_ = RemoteAssetFileStatus::UNSET;
/**
* Set if this is an online asset only.
*
* Note that this can also be set on online assets when their files have been downloaded
* locally. To distinguish between 'pure online' (so no file) and other cases, use the
* file_status_ field above.
*
* \see #AssetRepresentation::is_online_only()
*/
std::unique_ptr<OnlineAssetInfo> online_info_;
};
std::variant<ExternalAsset, ID *> asset_;
friend class AssetLibrary;
public:
/**
* Constructs an asset representation for an external ID stored on disk. The asset will not be
* editable.
*
* For online assets, use the version with #online_info below.
*/
AssetRepresentation(StringRef relative_asset_path,
StringRef name,
int id_type,
std::unique_ptr<AssetMetaData> metadata,
AssetLibrary &owner_asset_library);
/**
* Constructs an asset representation for an external ID stored online (requiring download).
*/
AssetRepresentation(StringRef relative_asset_path,
StringRef name,
int id_type,
std::unique_ptr<AssetMetaData> metadata,
AssetLibrary &owner_asset_library,
OnlineAssetInfo online_info);
/**
* Constructs an asset representation for an ID stored in the current file. This makes the asset
* local and fully editable.
*/
AssetRepresentation(ID &id, AssetLibrary &owner_asset_library);
~AssetRepresentation();
/**
* Create a weak reference for this asset that can be written to files, but can break under a
* number of conditions.
* A weak reference can only be created if an asset representation is owned by an asset library.
*/
AssetWeakReference make_weak_reference() const;
/**
* Makes sure the asset ready to load a preview, if necessary.
*
* For local IDs it calls #BKE_previewimg_id_get(). For others, this sets loading information
* to the preview but doesn't actually load it. To load it, attach its
* #PreviewImageRuntime::icon_id to a UI button (UI loads it asynchronously then) or call
* #BKE_previewimg_ensure() (not asynchronous).
*
* For online assets this triggers downloading of the preview.
*/
void ensure_previewable(const bContext &C, ReportList *reports = nullptr);
/**
* Get the preview of this asset.
*
* This will only return a preview for local ID assets or after #ensure_previewable() was
* called.
*/
PreviewImage *get_preview() const;
StringRefNull get_name() const;
ID_Type get_id_type() const;
AssetMetaData &get_metadata() const;
StringRefNull library_relative_identifier() const;
std::string full_path() const;
/**
* Return the absolute path of the blend file that contains this asset.
*
* Note that this performs a file-system check to see whether the blend file actually exists.
* If it does not, an empty string is returned. This generally shouldn't be an issue, but can
* happen, for example when the blend file is deleted and the asset browser not refreshed.
*
* This check is a necessity because data-blocks may have .blend and slashes in their name, and
* directory names may also end in `.blend`, resulting in an identifier like
* `directory.blend/Objects/filename.blend/Actions/hand/wave.blend/Actions/hi.blend`.
* Here the file is `directory.blend/Objects/filename.blend` and the asset is an Action named
* `hand/wave.blend/Actions/hi.blend`.
*/
std::string full_library_path() const;
/**
* For online assets (see #is_online_only()), the files that make up this asset.
*
* Will return an empty span if this is not an online asset.
*/
Span<OnlineAssetFile> online_asset_files() const;
/**
* Return the sum of sizes of all files associated with this asset, according to the listing.
*/
std::optional<int64_t> online_asset_files_combined_size_in_bytes() const;
/**
* For online assets (see #is_online_only()), the URL the asset's preview should be requested
* from.
*
* Will return an empty value if this is not an online asset.
*/
std::optional<StringRefNull> online_asset_preview_url() const;
/**
* For online assets (see #is_online_only()), the hash of the asset's preview.
*
* Will return an empty value if this is not an online asset.
*/
std::optional<StringRefNull> online_asset_preview_hash() const;
/**
* Turn the online asset into a normal asset. This removes the online data, and the "is online"
* marking, turning it into a regular on-disk asset.
*
* No-op if this is not an online asset.
*/
void online_asset_mark_downloaded();
/**
* Get the import method to use for this asset. A different one may be used if
* #may_override_import_method() returns true, otherwise, the returned value must be used. If
* there is no import method predefined for this asset no value is returned.
*/
std::optional<eAssetImportMethod> get_import_method() const;
/**
* Returns if this asset may be imported with an import method other than the one returned by
* #get_import_method(). Also returns true if there is no predefined import method
* (when #get_import_method() returns no value).
*/
bool may_override_import_method() const;
bool get_use_relative_path() const;
/**
* If this asset is stored inside this current file (#is_local_id() is true), this returns the
* ID's pointer, otherwise null.
*/
ID *local_id() const;
/** Returns if this asset is stored inside this current file, and as such fully editable. */
bool is_local_id() const;
/**
* The asset is purely stored online, there is no local file on disk for this.
*
* Regardless of what this function returns, there may be 'online info' (information from a
* remote asset listing) available, even when the file is on disk and this function returns
* `false`.
*
* \see #remote_file_status()
*/
bool is_online_only() const;
/**
* Returns whether the asset is stored in a probably-editable .asset.blend file.
*
* NOTE: This is suitable for poll functions (which should not open other files). The actual
* operator should still check that `G_FILE_ASSET_EDIT_FILE` / `Main::is_asset_edit_file` is set
* on the `.asset.blend` file (no utility function for this exists yet).
*
* NOTE: this function does cause _some_ disk I/O, as it checks one (or more) paths for
* existence. See #AssetRepresentation::full_library_path() for more info.
*
* If the asset is already imported, this check can be done via
* `bke::asset_edit_id_is_editable(asset_id)` and `bke::asset_edit_id_is_writable(asset_id)`.
*/
bool is_potentially_editable_asset_blend() const;
/**
* Status of this asset's on-disk file(s) compared to the remote listing.
* Returns #AssetFileStatus::UNSET if the asset has not been checked against a listing.
* For on-disk assets this reflects the status stamped after listing comparison.
* For online-only assets this reflects the status from #OnlineAssetInfo.
*/
RemoteAssetFileStatus remote_file_status() const;
/** Set the file status for on-disk assets. No-op for online-only assets. */
void remote_file_status_set(RemoteAssetFileStatus status);
/**
* Store the remote listings online info on an on-disk asset so it can be re-downloaded.
* Replaces any previously set online info.
*/
void online_info_set(OnlineAssetInfo info);
/**
* Return whether this asset requires (re-)downloading before it can be used.
*
* True for online-only assets (#is_online_only()) and for on-disk assets whose files no longer
* match the remote listing (e.g. #AssetFileStatus::NO_MATCH).
*/
bool needs_download() const;
AssetLibrary &owner_asset_library() const;
};
} // namespace asset_system
} // namespace blender

View File

@@ -0,0 +1,56 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup asset_system
*/
#pragma once
#include "BLI_string_ref.hh"
#include <memory>
#include <string>
namespace blender {
struct bContext;
}
namespace blender::asset_system {
/**
* C++ wrapper around the DiskFileHashService class implemented in Python.
*
* Run the following to see which hash algorithms are supported:
*
* `blender -b --python-expr "import hashlib; print(hashlib.algorithms_available)"`
*/
class DiskFileHashService {
private:
std::string storage_path_;
public:
explicit DiskFileHashService(StringRef storage_path);
~DiskFileHashService();
/** Return the hash of a file on disk. */
std::string get_hash(StringRef filepath, StringRef hash_algorithm);
/** Check the file on disk, to see if it matches the given properties. */
bool file_matches(StringRef filepath,
StringRef hash_algorithm,
StringRef hexhash,
int64_t size_in_bytes);
private:
/** Release the Python instance associated with this DFHS. */
void release_python();
};
/**
* Obtain a DiskFileHashService, which stores its cache at the given location.
*/
std::unique_ptr<DiskFileHashService> disk_file_hash_service_get(StringRef storage_path);
} // namespace blender::asset_system

View File

@@ -0,0 +1,39 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup asset_system
*/
#pragma once
#include "BLI_string_ref.hh"
namespace blender {
class UUID;
}
namespace blender::asset_system {
StringRefNull essentials_directory_path();
StringRefNull online_essentials_cache_directory_path();
StringRefNull online_essentials_url();
/**
* Check if the given URL matches the online essentials URL, with or without the optional
* `_asset-library-meta.json` ending. If the `.json` file name ending isn't present, the trailing
* slash is necessary for the URLs to match.
*/
bool is_online_essentials_url(StringRef url);
/**
* Check if the given absolute directory path is the online essentials cache path. If the path ends
* in a trailing slash, that's stripped before comparing.
*/
bool is_online_essentials_dirpath(StringRef dirpath);
/** Returns false for catalogs that are based on disabled experimental features. */
bool skip_experimental_asset_catalog(const UUID &catalog_id);
} // namespace blender::asset_system

View File

@@ -0,0 +1,264 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup asset_system
*/
#pragma once
#include <chrono>
#include <filesystem>
#include <optional>
#include "BLI_function_ref.hh"
#include "BLI_string_ref.hh"
#include "BLI_vector.hh"
#include "AS_asset_file_status.hh"
namespace blender {
struct bContext;
struct bUserAssetLibrary;
struct Main;
struct ReportList;
namespace asset_system {
struct RemoteLibraryDefinitionRef {
StringRefNull remote_url;
StringRefNull cache_dirpath;
RemoteLibraryDefinitionRef(const bUserAssetLibrary &library_definition);
RemoteLibraryDefinitionRef(StringRefNull remote_url, StringRefNull cache_dirpath)
: remote_url(remote_url), cache_dirpath(cache_dirpath)
{
}
};
constexpr StringRefNull REMOTE_LIBRARY_TOP_META_FILE_NAME = "_asset-library-meta.json";
constexpr StringRefNull REMOTE_LIBRARY_TOP_META_FILE_NAME_LEADING_SLASH =
"/_asset-library-meta.json";
/**
* Get the absolute file path to the `_asset-library-meta.json` of the given library's cache
* directory.
*/
std::string remote_library_top_meta_file_path(const RemoteLibraryDefinitionRef &library);
bool remote_library_url_ends_with_top_meta_file_name(const StringRef url);
/**
* Iterates all libraries registered in the Preferences and calls the given function with the URL
* of the library.
*
* \note Does not include the online essentials library.
*/
void foreach_registered_user_remote_library(FunctionRef<void(bUserAssetLibrary &)> fn);
/**
* Combination of a URL of a remote resource, and its hash.
*/
struct URLWithHash {
std::string url;
/** String in the form `{HASH_TYPE}:{HASH_VALUE}`. */
std::string hash;
};
/** Information of a single file of an online asset. */
struct OnlineAssetFile {
/**
* The path within the asset library this file should be downloaded to.
* Relative to the library root.
*/
std::string path;
int64_t size_in_bytes;
/** The URL the asset should be downloaded from. */
URLWithHash url;
};
/**
* Information specific to online assets.
*
* This is constructed from the remote asset listing and contains all data needed to download and
* verify related fragments. #AssetRepresentation stores this for online assets.
*/
struct OnlineAssetInfo {
/**
* The files for this asset.
* The first one contains the asset data-blocks, and subsequent files are dependencies.
*/
Vector<OnlineAssetFile> files;
std::optional<URLWithHash> preview_url;
/**
* Return the asset's main file, i.e. the file containing the asset data-block.
*
* This can only return an empty string in error cases, i.e. when the `files` vector (see above)
* is empty. This should never happen; file-less assets should be rejected when loading the
* listing.
*
* NOTE: Blender currently only has preliminary support for multi-file assets (it downloads them
* correctly, but there's little in place to check for conflicting versions, or to handle things
* like copying non-blend files to the project directory). Even though the 'files' list will
* likely only have one element (at least that is the case at the time of writing), this function
* should not be used as a shortcut when trying to obtain "the asset's files".
*/
StringRefNull asset_file() const;
};
class AssetRepresentation;
float remote_library_total_asset_downloads_progress();
/** Return true if there is any asset file (any file in an assets file set) being downloaded. */
bool remote_library_has_unfinished_asset_downloads();
/**
* Ensures the remote library cache directory exists, and calls the Python downloader. Doesn't do
* anything if a download with the library's URL is already ongoing.
*/
void remote_library_request_download(const RemoteLibraryDefinitionRef &library_definition);
void remote_library_cancel_all_listing_downloads(const bContext &C);
void remote_library_request_asset_download(const bContext &C,
const AssetRepresentation &asset,
ReportList *reports);
void remote_library_request_preview_download(const bContext &C,
const AssetRepresentation &asset,
const StringRef dst_filepath,
ReportList *reports);
void remote_library_cancel_all_asset_downloads(bContext &C);
/**
* Get the absolute path to an online library's cache directory using \a library_dirname as library
* identifier.
*
* The path is the general cache directory (e.g. `$HOME/.cache/blender/remote-assets/`) plus the
* \a library_dirname as subdirectory.
*
* The resulting path will be shortened to #FILE_MAXDIR if necessary.
*/
std::string remote_library_cache_directory_path(StringRefNull library_dirname);
/**
* Determine the absolute path of the asset library's on-disk cache directory for downloaded files,
* based on the library's URL.
*
* The path is the general cache directory (e.g. `$HOME/.cache/blender/remote-assets/`) plus a
* shortened MD5 hash of the remote URL to identify the library.
*
* This is based on the remote URL of the library, and not the library name, as the name can be
* user-chosen, so the URL is a more stable identifier. And if there happen to be multiple
* libraries in the preferences, with the same URL, they'll share the same cache.
*
* The resulting path will be shortened to #FILE_MAXDIR if necessary.
*/
std::string remote_library_cache_directory_path_from_url(StringRef remote_url);
/**
* Get the absolute file path the preview for \a asset is expected at once downloaded.
*
* The path is built like this:
* - Online library cache directory (e.g.
* `$HOME/.cache/blender/remote-assets/1a2b3c-my.assets.com/`)
* - `_thumbs/large/`
* - The first two characters of the MD5 hash of the full asset path
* (#AssetRepresentation.full_path()).
* - The next 30 characters of the MD5 hash.
* - If the download URL of the preview has an extension (some string after a period), up to 6
* characters of that extension. (Previews load fine regardless of the extension. But the
* extension is still a useful indicator, and some file browsers can display previews that way.)
*
* The reason hashes are used within `_thumbs/large/` instead of the relative path of the asset (or
* another relative path derived from the preview URL) is to keep paths short enough to not violate
* path length limitations.
*/
std::string remote_library_asset_preview_path(const AssetRepresentation &asset);
/**
* Status information about an externally loaded asset library listing, stored globally.
*
* Remote asset library downloading is handled in Python. This API allows storing status
* information globally per URL. Asset UIs can then query the status and reflect it accordingly.
*
* Another important use is coordinating the Python side downloading with the C++ side loading.
* The C++ asset library loading might have to wait for Python to be done downloading and
* validating individual asset listing pages, and load in these new pages as they become ready.
*
* All functions must be called on the same thread.
*/
class RemoteLibraryLoadingStatus {
public:
enum Status {
Loading,
Finished,
Failure,
Cancelled,
};
using TimePoint = std::chrono::time_point<std::chrono::steady_clock>;
using FileSystemTimePoint = std::filesystem::file_time_type;
private:
float timeout_ = 0.0f;
FileSystemTimePoint loading_start_time_point_ = {};
TimePoint last_updated_time_point_ = {};
/* See #RemoteLibraryLoadingStatus::handle_timeout(). */
TimePoint last_timeout_handled_time_point_ = {};
TimePoint last_new_pages_time_point_ = {};
std::optional<Status> status_ = std::nullopt;
std::optional<StringRefNull> failure_message_ = std::nullopt;
bool metafiles_in_place_ = false;
public:
static void begin_loading(StringRef url, float timeout);
/** Let the state know that the loading is still ongoing, resetting the timeout. */
static void ping_still_loading(StringRef url);
static void ping_new_pages(StringRef url);
static void ping_new_preview(const bContext &C, StringRef preview_full_filepath);
static void ping_asset_file_progress(StringRef absolute_file_url, int64_t size_in_bytes);
/** Should be called when an asset file download has completed successfully. */
static void ping_asset_file_download_succeeded(const bContext &C,
StringRef library_url,
StringRef absolute_file_url,
StringRef local_file_abspath);
/** Should be called when an asset file download has failed. Partial progress for the file is
* reset to zero, since a future retry has to start from scratch. */
static void ping_asset_file_download_failed(const bContext &C,
StringRef library_url,
StringRef absolute_file_url,
StringRef local_file_abspath);
/** Inform the asset system that there are no more pending asset file downloads for any asset
* library. */
static void ping_download_queue_done(const bContext &C);
static void ping_metafiles_in_place(StringRef url);
static void set_finished(StringRef url);
static void set_cancelled(const StringRef url);
static void set_failure(StringRef url, std::optional<StringRefNull> failure_message);
static std::optional<StringRefNull> failure_message(StringRef url);
static std::optional<RemoteLibraryLoadingStatus::Status> status(StringRef url);
static std::optional<bool> metafiles_in_place(StringRef url);
static std::optional<FileSystemTimePoint> loading_start_time(const StringRef url);
static std::optional<TimePoint> last_new_pages_time(StringRef url);
/**
* Checks if the status storage timed out, because it hasn't received status updates for the
* given timeout duration. Changes the status to failure in that case.
*
* Note that this function doesn't do more than check if the timeout is reached, and changing
* state to failure if so. It's meant to be called in regular, short intervals to make the whole
* timeout handling work. Current remote asset library loading takes care of this.
*
* \return True if the loading status switched to #Status::Failure due to timing out.
*/
static bool handle_timeout(StringRef url);
private:
/** Update the last update time point, effectively resetting the time-out timer. */
void reset_timeout();
};
} // namespace asset_system
} // namespace blender

View File

@@ -0,0 +1,107 @@
# SPDX-FileCopyrightText: 2023 Blender Authors
#
# SPDX-License-Identifier: GPL-2.0-or-later
set(INC
PUBLIC .
intern
intern/library_types
../makesrna
../editors/include
# RNA_prototypes.hh
${CMAKE_BINARY_DIR}/source/blender/makesrna
)
set(INC_SYS
)
set(SRC
intern/asset_catalog.cc
intern/asset_catalog_collection.cc
intern/asset_catalog_definition_file.cc
intern/asset_catalog_path.cc
intern/asset_catalog_tree.cc
intern/asset_library.cc
intern/asset_library_service.cc
intern/asset_representation.cc
intern/disk_file_hash_service.cc
intern/library_types/all_library.cc
intern/library_types/common.cc
intern/library_types/essentials_library.cc
intern/library_types/on_disk_library.cc
intern/library_types/preferences_on_disk_library.cc
intern/library_types/remote_library.cc
intern/library_types/runtime_library.cc
intern/utils.cc
AS_asset_catalog.hh
AS_asset_catalog_path.hh
AS_asset_catalog_tree.hh
AS_asset_library.hh
AS_asset_representation.hh
AS_disk_file_hash_service.hh
AS_essentials_library.hh
AS_remote_library.hh
intern/asset_catalog_collection.hh
intern/asset_catalog_definition_file.hh
intern/asset_library_service.hh
intern/library_types/all_library.hh
intern/library_types/common.hh
intern/library_types/essentials_library.hh
intern/library_types/on_disk_library.hh
intern/library_types/preferences_on_disk_library.hh
intern/library_types/remote_library.hh
intern/library_types/runtime_library.hh
intern/utils.hh
)
set(LIB
PRIVATE bf::blenkernel
PRIVATE bf::blenlib
PRIVATE bf::blentranslation
PRIVATE bf::dna
PRIVATE bf::imbuf
PRIVATE bf::intern::clog
PRIVATE bf::intern::guardedalloc
PRIVATE bf::windowmanager
)
if(WITH_PYTHON)
list(APPEND INC
../python
)
add_definitions(-DWITH_PYTHON)
endif()
blender_add_lib(bf_asset_system "${SRC}" "${INC}" "${INC_SYS}" "${LIB}")
add_library(bf::asset_system ALIAS bf_asset_system)
if(WITH_GTESTS)
set(TEST_INC
../editors/asset
)
set(TEST_SRC
tests/asset_catalog_path_test.cc
tests/asset_catalog_test.cc
tests/asset_catalog_tree_test.cc
tests/asset_library_service_test.cc
tests/asset_library_test.cc
tests/asset_representation_test.cc
tests/essentials_library_test.cc
tests/remote_library_test.cc
)
set(TEST_COMMON_SRC
tests/asset_library_test_common.hh
)
set(TEST_LIB
bf_asset_system
PRIVATE bf_editor_asset
)
blender_add_test_suite_lib(asset_system
"${TEST_SRC}" "${INC};${TEST_INC}" "${INC_SYS}" "${LIB};${TEST_LIB}" "${TEST_COMMON_SRC}"
)
endif()
# RNA_prototypes.hh dna_type_offsets.h
add_dependencies(bf_asset_system bf_rna)

View File

@@ -0,0 +1,761 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup asset_system
*/
#include <iostream>
#include <set>
#include "AS_asset_catalog.hh"
#include "AS_asset_catalog_tree.hh"
#include "AS_asset_library.hh"
#include "AS_essentials_library.hh"
#include "asset_catalog_collection.hh"
#include "asset_catalog_definition_file.hh"
#include "BLI_fileops.h"
#include "BLI_path_utils.hh"
/* For S_ISREG() and S_ISDIR() on Windows. */
#ifdef WIN32
# include "BLI_winstuff.h"
#endif
#include "asset_library_service.hh"
#include "CLG_log.h"
namespace blender {
static CLG_LogRef LOG = {"asset.catalog"};
namespace asset_system {
const CatalogFilePath AssetCatalogService::DEFAULT_CATALOG_FILENAME = "blender_assets.cats.txt";
AssetCatalogService::AssetCatalogService(const CatalogFilePath &asset_library_root,
std::optional<read_only_tag> read_only_tag)
: catalog_collection_(std::make_unique<AssetCatalogCollection>()),
asset_library_root_(asset_library_root),
is_read_only_(read_only_tag ? true : false)
{
}
AssetCatalogService::AssetCatalogService(read_only_tag /*unused*/) : AssetCatalogService()
{
const_cast<bool &>(is_read_only_) = true;
}
AssetCatalogService::~AssetCatalogService() = default;
void AssetCatalogService::tag_has_unsaved_changes(AssetCatalog *edited_catalog)
{
BLI_assert(!is_read_only_);
if (edited_catalog) {
edited_catalog->flags.has_unsaved_changes = true;
}
BLI_assert(catalog_collection_);
catalog_collection_->has_unsaved_changes_ = true;
}
void AssetCatalogService::untag_has_unsaved_changes()
{
BLI_assert(catalog_collection_);
catalog_collection_->has_unsaved_changes_ = false;
/* TODO(Sybren): refactor; this is more like "post-write cleanup" than "remove a tag" code. */
/* Forget about any deleted catalogs. */
if (catalog_collection_->catalog_definition_file_) {
for (CatalogID catalog_id : catalog_collection_->deleted_catalogs_.keys()) {
catalog_collection_->catalog_definition_file_->forget(catalog_id);
}
}
catalog_collection_->deleted_catalogs_.clear();
/* Mark all remaining catalogs as "without unsaved changes". */
for (auto &catalog_uptr : catalog_collection_->catalogs_.values()) {
catalog_uptr->flags.has_unsaved_changes = false;
}
}
bool AssetCatalogService::has_unsaved_changes() const
{
BLI_assert(catalog_collection_);
return catalog_collection_->has_unsaved_changes_;
}
bool AssetCatalogService::is_read_only() const
{
return is_read_only_;
}
void AssetCatalogService::tag_all_catalogs_as_unsaved_changes()
{
for (auto &catalog : catalog_collection_->catalogs_.values()) {
catalog->flags.has_unsaved_changes = true;
}
catalog_collection_->has_unsaved_changes_ = true;
}
bool AssetCatalogService::is_empty() const
{
BLI_assert(catalog_collection_);
return catalog_collection_->catalogs_.is_empty();
}
const OwningAssetCatalogMap &AssetCatalogService::get_catalogs() const
{
return catalog_collection_->catalogs_;
}
const OwningAssetCatalogMap &AssetCatalogService::get_deleted_catalogs() const
{
return catalog_collection_->deleted_catalogs_;
}
const AssetCatalogDefinitionFile *AssetCatalogService::get_catalog_definition_file() const
{
return catalog_collection_->catalog_definition_file_.get();
}
AssetCatalog *AssetCatalogService::find_catalog(CatalogID catalog_id) const
{
const std::unique_ptr<AssetCatalog> *catalog_uptr_ptr =
catalog_collection_->catalogs_.lookup_ptr(catalog_id);
if (catalog_uptr_ptr == nullptr) {
return nullptr;
}
return catalog_uptr_ptr->get();
}
AssetCatalog *AssetCatalogService::find_catalog_by_path(const AssetCatalogPath &path) const
{
/* Use an AssetCatalogOrderedSet to find the 'best' catalog for this path. This will be the first
* one loaded from disk, or if that does not exist the one with the lowest UUID. This ensures
* stable, predictable results. */
MutableAssetCatalogOrderedSet ordered_catalogs;
for (const auto &catalog : catalog_collection_->catalogs_.values()) {
if (catalog->path == path) {
ordered_catalogs.insert(catalog.get());
}
}
if (ordered_catalogs.empty()) {
return nullptr;
}
MutableAssetCatalogOrderedSet::iterator best_choice_it = ordered_catalogs.begin();
return *best_choice_it;
}
bool AssetCatalogService::is_catalog_known(CatalogID catalog_id) const
{
BLI_assert(catalog_collection_);
return catalog_collection_->catalogs_.contains(catalog_id);
}
AssetCatalogFilter AssetCatalogService::create_catalog_filter(
const CatalogID active_catalog_id) const
{
Set<CatalogID> matching_catalog_ids;
Set<CatalogID> known_catalog_ids;
matching_catalog_ids.add(active_catalog_id);
const AssetCatalog *active_catalog = this->find_catalog(active_catalog_id);
/* This cannot just iterate over tree items to get all the required data, because tree items only
* represent single UUIDs. It could be used to get the main UUIDs of the children, though, and
* then only do an exact match on the path (instead of the more complex `is_contained_in()`
* call). Without an extra indexed-by-path acceleration structure, this is still going to require
* a linear search, though. */
for (const auto &catalog_uptr : catalog_collection_->catalogs_.values()) {
if (active_catalog && catalog_uptr->path.is_contained_in(active_catalog->path)) {
matching_catalog_ids.add(catalog_uptr->catalog_id);
}
known_catalog_ids.add(catalog_uptr->catalog_id);
}
return AssetCatalogFilter(std::move(matching_catalog_ids), std::move(known_catalog_ids));
}
void AssetCatalogService::delete_catalog_by_id_soft(const CatalogID catalog_id)
{
std::unique_ptr<AssetCatalog> *catalog_uptr_ptr = catalog_collection_->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 catalog_collection_->catalogs_ to catalog_collection_->deleted_catalogs_.
*/
catalog_collection_->deleted_catalogs_.add(catalog_id, std::move(*catalog_uptr_ptr));
/* The catalog can now be removed from the map without freeing the actual AssetCatalog. */
catalog_collection_->catalogs_.remove(catalog_id);
}
void AssetCatalogService::delete_catalog_by_id_hard(CatalogID catalog_id)
{
catalog_collection_->catalogs_.remove(catalog_id);
catalog_collection_->deleted_catalogs_.remove(catalog_id);
/* TODO(@sybren): adjust this when supporting multiple CDFs. */
catalog_collection_->catalog_definition_file_->forget(catalog_id);
}
void AssetCatalogService::prune_catalogs_by_path(const AssetCatalogPath &path)
{
/* Build a collection of catalog IDs to delete. */
Set<CatalogID> catalogs_to_delete;
for (const auto &catalog_uptr : catalog_collection_->catalogs_.values()) {
const AssetCatalog *cat = catalog_uptr.get();
if (cat->path.is_contained_in(path)) {
catalogs_to_delete.add(cat->catalog_id);
}
}
/* Delete the catalogs. */
for (const CatalogID cat_id : catalogs_to_delete) {
this->delete_catalog_by_id_soft(cat_id);
}
this->invalidate_catalog_tree();
AssetLibraryService::get()->tag_all_library_catalogs_dirty();
}
void AssetCatalogService::prune_catalogs_by_id(const CatalogID catalog_id)
{
const AssetCatalog *catalog = find_catalog(catalog_id);
BLI_assert_msg(catalog, "trying to prune asset catalogs by the path of a non-existent catalog");
if (!catalog) {
return;
}
this->prune_catalogs_by_path(catalog->path);
}
void AssetCatalogService::update_catalog_path(const CatalogID catalog_id,
const AssetCatalogPath &new_catalog_path)
{
AssetCatalog *renamed_cat = this->find_catalog(catalog_id);
const AssetCatalogPath old_cat_path = renamed_cat->path;
for (auto &catalog_uptr : catalog_collection_->catalogs_.values()) {
AssetCatalog *cat = catalog_uptr.get();
const AssetCatalogPath new_path = cat->path.rebase(old_cat_path, new_catalog_path);
if (!new_path) {
continue;
}
cat->path = new_path;
cat->simple_name_refresh();
this->tag_has_unsaved_changes(cat);
/* TODO(Sybren): go over all assets that are assigned to this catalog, defined in the current
* blend file, and update the catalog simple name stored there. */
}
this->create_missing_catalogs();
this->invalidate_catalog_tree();
AssetLibraryService::get()->tag_all_library_catalogs_dirty();
}
AssetCatalog *AssetCatalogService::create_catalog(const AssetCatalogPath &catalog_path)
{
std::unique_ptr<AssetCatalog> catalog = AssetCatalog::from_path(catalog_path);
catalog->flags.has_unsaved_changes = true;
/* 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(!catalog_collection_->catalogs_.contains(catalog->catalog_id),
"duplicate catalog ID not supported");
catalog_collection_->catalogs_.add_new(catalog->catalog_id, std::move(catalog));
if (catalog_collection_->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_collection_->catalog_definition_file_->add_new(catalog_ptr);
}
this->invalidate_catalog_tree();
AssetLibraryService::get()->tag_all_library_catalogs_dirty();
return catalog_ptr;
}
static std::string asset_definition_default_file_path_from_dir(StringRef asset_library_root)
{
char file_path[PATH_MAX];
BLI_path_join(file_path,
sizeof(file_path),
asset_library_root.data(),
AssetCatalogService::DEFAULT_CATALOG_FILENAME.data());
return file_path;
}
void AssetCatalogService::load_from_disk()
{
this->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) {
/* It's fine if the catalogs file doesn't exist, it just means there are no catalogs. */
CLOG_DEBUG(&LOG, "path not found: %s", file_or_directory_path.data());
return;
}
if (S_ISREG(status.st_mode)) {
this->load_single_file(file_or_directory_path);
}
else if (S_ISDIR(status.st_mode)) {
this->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? */
this->create_missing_catalogs();
this->invalidate_catalog_tree();
}
void AssetCatalogService::add_from_existing(
const AssetCatalogService &other_service,
AssetCatalogCollection::OnDuplicateCatalogIdFn on_duplicate_items)
{
catalog_collection_->add_catalogs_from_existing(*other_service.catalog_collection_,
on_duplicate_items);
}
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. */
CLOG_DEBUG(&LOG, "path not found: %s", file_path.data());
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(!catalog_collection_->catalog_definition_file_,
"Only loading of a single catalog definition file is supported.");
catalog_collection_->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>(catalog_definition_file_path);
/* TODO(Sybren): this might have to move to a higher level when supporting multiple CDFs. */
Set<AssetCatalogPath> seen_paths;
auto catalog_parsed_callback = [this, catalog_definition_file_path, &seen_paths](
std::unique_ptr<AssetCatalog> catalog) {
if (skip_experimental_asset_catalog(catalog->catalog_id)) {
return false;
}
if (catalog_collection_->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;
}
catalog->flags.is_first_loaded = seen_paths.add(catalog->path);
/* The AssetCatalog pointer is now owned by the AssetCatalogService. */
catalog_collection_->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::reload_catalogs()
{
/* TODO(Sybren): expand to support multiple CDFs. */
AssetCatalogDefinitionFile *const cdf = catalog_collection_->catalog_definition_file_.get();
if (!cdf || cdf->file_path.empty() || !BLI_is_file(cdf->file_path.c_str())) {
return;
}
/* Keeps track of the catalog IDs that are seen in the CDF, so that we also know what was deleted
* from the file on disk. */
Set<CatalogID> cats_in_file;
auto catalog_parsed_callback = [this, &cats_in_file](std::unique_ptr<AssetCatalog> catalog) {
if (skip_experimental_asset_catalog(catalog->catalog_id)) {
return false;
}
const CatalogID catalog_id = catalog->catalog_id;
cats_in_file.add(catalog_id);
const bool should_skip = this->is_catalog_known_with_unsaved_changes(catalog_id);
if (should_skip) {
/* Do not overwrite unsaved local changes. */
return false;
}
/* This is either a new catalog, or we can just replace the in-memory one with the newly loaded
* one. */
catalog_collection_->catalogs_.add_overwrite(catalog_id, std::move(catalog));
return true;
};
cdf->parse_catalog_file(cdf->file_path, catalog_parsed_callback);
this->purge_catalogs_not_listed(cats_in_file);
this->create_missing_catalogs();
this->invalidate_catalog_tree();
}
void AssetCatalogService::purge_catalogs_not_listed(const Set<CatalogID> &catalogs_to_keep)
{
Set<CatalogID> cats_to_remove;
for (CatalogID cat_id : this->catalog_collection_->catalogs_.keys()) {
if (catalogs_to_keep.contains(cat_id)) {
continue;
}
if (this->is_catalog_known_with_unsaved_changes(cat_id)) {
continue;
}
/* This catalog is not on disk, but also not modified, so get rid of it. */
cats_to_remove.add(cat_id);
}
for (CatalogID cat_id : cats_to_remove) {
this->delete_catalog_by_id_hard(cat_id);
}
}
bool AssetCatalogService::is_catalog_known_with_unsaved_changes(const CatalogID catalog_id) const
{
if (catalog_collection_->deleted_catalogs_.contains(catalog_id)) {
/* Deleted catalogs are always considered modified, by definition. */
return true;
}
const std::unique_ptr<AssetCatalog> *catalog_uptr_ptr =
catalog_collection_->catalogs_.lookup_ptr(catalog_id);
if (!catalog_uptr_ptr) {
/* Catalog is unknown. */
return false;
}
const bool has_unsaved_changes = (*catalog_uptr_ptr)->flags.has_unsaved_changes;
return has_unsaved_changes;
}
bool AssetCatalogService::write_to_disk(const CatalogFilePath &blend_file_path)
{
/* The caller should probably check this somewhat earlier and properly disable whatever operation
* triggers the writing. */
BLI_assert(!is_read_only_);
if (is_read_only_) {
return false;
}
if (!this->write_to_disk_ex(blend_file_path)) {
return false;
}
this->untag_has_unsaved_changes();
this->invalidate_catalog_tree();
return true;
}
bool AssetCatalogService::write_to_disk_ex(const CatalogFilePath &blend_file_path)
{
/* TODO(Sybren): expand to support multiple CDFs. */
/* - Already loaded a CDF from disk? -> Only write to that file when there were actual changes.
* This prevents touching the file, which can cause issues when multiple Blender instances are
* accessing the same file (like on shared storage, Sync-thing, etc.). See #111576.
*/
if (catalog_collection_->catalog_definition_file_) {
/* Always sync with what's on disk. */
this->reload_catalogs();
if (!this->has_unsaved_changes() &&
catalog_collection_->catalog_definition_file_->exists_on_disk())
{
return true;
}
return catalog_collection_->catalog_definition_file_->write_to_disk();
}
if (catalog_collection_->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. */
}
const CatalogFilePath cdf_path_to_write = find_suitable_cdf_path_for_writing(blend_file_path);
catalog_collection_->catalog_definition_file_ = construct_cdf_in_memory(cdf_path_to_write);
this->reload_catalogs();
return catalog_collection_->catalog_definition_file_->write_to_disk();
}
void AssetCatalogService::prepare_to_merge_on_write()
{
/* TODO(Sybren): expand to support multiple CDFs. */
if (!catalog_collection_->catalog_definition_file_) {
/* There is no CDF connected, so it's a no-op. */
return;
}
/* Remove any association with the CDF, so that a new location will be chosen
* when the blend file is saved. */
catalog_collection_->catalog_definition_file_.reset();
/* Mark all in-memory catalogs as "dirty", to force them to be kept around on
* the next "load-merge-write" cycle. */
this->tag_all_catalogs_as_unsaved_changes();
}
CatalogFilePath AssetCatalogService::find_suitable_cdf_path_for_writing(
const CatalogFilePath &blend_file_path)
{
BLI_assert_msg(!blend_file_path.empty(),
"A non-empty .blend file path is required to be able to determine where the "
"catalog definition file should be put");
/* Ask the asset library API for an appropriate location. */
const std::string suitable_root_path = AS_asset_library_find_suitable_root_path_from_path(
blend_file_path);
if (!suitable_root_path.empty()) {
char asset_lib_cdf_path[PATH_MAX];
BLI_path_join(asset_lib_cdf_path,
sizeof(asset_lib_cdf_path),
suitable_root_path.c_str(),
DEFAULT_CATALOG_FILENAME.c_str());
return asset_lib_cdf_path;
}
/* Determine the default CDF path in the same directory of the blend file. */
char blend_dir_path[PATH_MAX];
BLI_path_split_dir_part(blend_file_path.c_str(), blend_dir_path, sizeof(blend_dir_path));
const CatalogFilePath cdf_path_next_to_blend = asset_definition_default_file_path_from_dir(
blend_dir_path);
return cdf_path_next_to_blend;
}
std::unique_ptr<AssetCatalogDefinitionFile> AssetCatalogService::construct_cdf_in_memory(
const CatalogFilePath &file_path) const
{
auto cdf = std::make_unique<AssetCatalogDefinitionFile>(file_path);
for (auto &catalog : catalog_collection_->catalogs_.values()) {
cdf->add_new(catalog.get());
}
return cdf;
}
std::unique_ptr<AssetCatalogTree> AssetCatalogService::read_into_tree() const
{
auto tree = std::make_unique<AssetCatalogTree>();
/* Go through the catalogs, insert each path component into the tree where needed. */
for (auto &catalog : catalog_collection_->catalogs_.values()) {
tree->insert_item(*catalog);
}
return tree;
}
void AssetCatalogService::invalidate_catalog_tree()
{
std::lock_guard lock{catalog_tree_mutex_};
this->catalog_tree_ = nullptr;
}
std::shared_ptr<const AssetCatalogTree> AssetCatalogService::catalog_tree()
{
std::lock_guard lock{catalog_tree_mutex_};
if (!catalog_tree_) {
/* Ensure all catalog paths lead to valid catalogs. This is important for the catalog tree to
* be usable, e.g. it makes sure every item in the tree maps to an actual catalog. */
this->create_missing_catalogs();
catalog_tree_ = read_into_tree();
}
return catalog_tree_;
}
void AssetCatalogService::create_missing_catalogs()
{
/* Construct an ordered set of paths to check, so that parents are ordered before children. */
std::set<AssetCatalogPath> paths_to_check;
for (auto &catalog : catalog_collection_->catalogs_.values()) {
paths_to_check.insert(catalog->path);
}
std::set<AssetCatalogPath> seen_paths;
/* The empty parent should never be created, so always be considered "seen". */
seen_paths.insert(AssetCatalogPath(""));
/* Find and create missing direct parents (so ignoring parents-of-parents). */
while (!paths_to_check.empty()) {
/* Pop the first path of the queue. */
const AssetCatalogPath path = *paths_to_check.begin();
paths_to_check.erase(paths_to_check.begin());
if (seen_paths.contains(path)) {
/* This path has been seen already, so it can be ignored. */
continue;
}
seen_paths.insert(path);
const AssetCatalogPath parent_path = path.parent();
if (seen_paths.contains(parent_path)) {
/* The parent exists, continue to the next path. */
continue;
}
/* The parent doesn't exist, so create it and queue it up for checking its parent. */
AssetCatalog *parent_catalog = this->create_catalog(parent_path);
parent_catalog->flags.has_unsaved_changes = true;
paths_to_check.insert(parent_path);
}
/* TODO(Sybren): bind the newly created catalogs to a CDF, if we know about it. */
}
bool AssetCatalogService::is_undo_possbile() const
{
return !undo_snapshots_.is_empty();
}
bool AssetCatalogService::is_redo_possbile() const
{
return !redo_snapshots_.is_empty();
}
void AssetCatalogService::undo()
{
BLI_assert_msg(is_undo_possbile(), "Undo stack is empty");
redo_snapshots_.append(std::move(catalog_collection_));
catalog_collection_ = undo_snapshots_.pop_last();
this->create_missing_catalogs();
this->invalidate_catalog_tree();
AssetLibraryService::get()->tag_all_library_catalogs_dirty();
}
void AssetCatalogService::redo()
{
BLI_assert(!is_read_only_);
BLI_assert_msg(is_redo_possbile(), "Redo stack is empty");
undo_snapshots_.append(std::move(catalog_collection_));
catalog_collection_ = redo_snapshots_.pop_last();
this->create_missing_catalogs();
this->invalidate_catalog_tree();
AssetLibraryService::get()->tag_all_library_catalogs_dirty();
}
void AssetCatalogService::undo_push()
{
BLI_assert(!is_read_only_);
std::unique_ptr<AssetCatalogCollection> snapshot = catalog_collection_->deep_copy();
undo_snapshots_.append(std::move(snapshot));
redo_snapshots_.clear();
}
/* ---------------------------------------------------------------------- */
AssetCatalog::AssetCatalog(const CatalogID catalog_id,
const AssetCatalogPath &path,
const std::string &simple_name)
: catalog_id(catalog_id), path(path), simple_name(simple_name)
{
}
std::unique_ptr<AssetCatalog> AssetCatalog::from_path(const AssetCatalogPath &path)
{
const AssetCatalogPath clean_path = path.cleanup();
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;
}
void AssetCatalog::simple_name_refresh()
{
this->simple_name = sensible_simple_name_for_path(this->path);
}
std::string AssetCatalog::sensible_simple_name_for_path(const AssetCatalogPath &path)
{
std::string name = path.str();
std::replace(name.begin(), name.end(), AssetCatalogPath::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);
}
/* ---------------------------------------------------------------------- */
AssetCatalogFilter::AssetCatalogFilter(Set<CatalogID> &&matching_catalog_ids,
Set<CatalogID> &&known_catalog_ids)
: matching_catalog_ids_(std::move(matching_catalog_ids)),
known_catalog_ids_(std::move(known_catalog_ids))
{
}
bool AssetCatalogFilter::contains(const CatalogID asset_catalog_id) const
{
return matching_catalog_ids_.contains(asset_catalog_id);
}
bool AssetCatalogFilter::is_known(const CatalogID asset_catalog_id) const
{
if (BLI_uuid_is_nil(asset_catalog_id)) {
return false;
}
return known_catalog_ids_.contains(asset_catalog_id);
}
} // namespace asset_system
} // namespace blender

View File

@@ -0,0 +1,73 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup asset_system
*/
#include "asset_catalog_definition_file.hh"
#include "asset_catalog_collection.hh"
namespace blender::asset_system {
bool AssetCatalogCollection::is_empty() const
{
return catalogs_.is_empty() && deleted_catalogs_.is_empty();
}
std::unique_ptr<AssetCatalogCollection> AssetCatalogCollection::deep_copy() const
{
auto copy = std::make_unique<AssetCatalogCollection>();
copy->has_unsaved_changes_ = this->has_unsaved_changes_;
copy->catalogs_ = copy_catalog_map(this->catalogs_);
copy->deleted_catalogs_ = copy_catalog_map(this->deleted_catalogs_);
if (catalog_definition_file_) {
copy->catalog_definition_file_ = catalog_definition_file_->copy_and_remap(
copy->catalogs_, copy->deleted_catalogs_);
}
return copy;
}
static void copy_catalog_map_into_existing(
const OwningAssetCatalogMap &source,
OwningAssetCatalogMap &dest,
AssetCatalogCollection::OnDuplicateCatalogIdFn on_duplicate_items)
{
for (const auto &orig_catalog_uptr : source.values()) {
if (dest.contains(orig_catalog_uptr->catalog_id)) {
if (on_duplicate_items) {
on_duplicate_items(*dest.lookup(orig_catalog_uptr->catalog_id), *orig_catalog_uptr);
}
continue;
}
auto copy_catalog_uptr = std::make_unique<AssetCatalog>(*orig_catalog_uptr);
dest.add_new(copy_catalog_uptr->catalog_id, std::move(copy_catalog_uptr));
}
}
void AssetCatalogCollection::add_catalogs_from_existing(
const AssetCatalogCollection &other,
AssetCatalogCollection::OnDuplicateCatalogIdFn on_duplicate_items)
{
copy_catalog_map_into_existing(other.catalogs_, catalogs_, on_duplicate_items);
}
OwningAssetCatalogMap AssetCatalogCollection::copy_catalog_map(const OwningAssetCatalogMap &orig)
{
OwningAssetCatalogMap copy;
copy_catalog_map_into_existing(
orig, copy, /*on_duplicate_items=*/[](const AssetCatalog &, const AssetCatalog &) {
/* `copy` was empty before. If this happens it means there was a duplicate in the `orig`
* catalog map which should've been caught already. */
BLI_assert_unreachable();
});
return copy;
}
} // namespace blender::asset_system

View File

@@ -0,0 +1,68 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup asset_system
*/
#pragma once
#include "AS_asset_catalog.hh"
namespace blender::asset_system {
class AssetLibraryService;
/**
* All catalogs that are owned by a single asset library, and managed by a single instance of
* #AssetCatalogService. The undo system for asset catalog edits contains historical copies of this
* struct.
*/
class AssetCatalogCollection {
protected:
/** All catalogs known, except the known-but-deleted ones. */
OwningAssetCatalogMap catalogs_;
/** Catalogs that have been deleted. They are kept around so that the load-merge-save of catalog
* definition files can actually delete them if they already existed on disk (instead of the
* merge operation resurrecting them). */
OwningAssetCatalogMap deleted_catalogs_;
/* For now only a single catalog definition file is supported.
* The aim is to support an arbitrary number of such files per asset library in the future. */
std::unique_ptr<AssetCatalogDefinitionFile> catalog_definition_file_;
/** Whether any of the catalogs have unsaved changes. */
bool has_unsaved_changes_ = false;
friend AssetCatalogService;
friend AssetLibraryService;
public:
AssetCatalogCollection() = default;
AssetCatalogCollection(const AssetCatalogCollection &other) = delete;
AssetCatalogCollection(AssetCatalogCollection &&other) noexcept = default;
/** Check if this contains any catalogs or deleted catalogs. Doesn't check if a CDF is present.
*/
bool is_empty() const;
std::unique_ptr<AssetCatalogCollection> deep_copy() const;
using OnDuplicateCatalogIdFn =
FunctionRef<void(const AssetCatalog &existing, const AssetCatalog &to_be_ignored)>;
/**
* Copy the catalogs from \a other and append them to this collection. Copies no other data
* otherwise.
*
* \note If a catalog from \a other already exists in this collection (identified by catalog ID),
* it will be skipped and \a on_duplicate_items will be called.
*/
void add_catalogs_from_existing(const AssetCatalogCollection &other,
OnDuplicateCatalogIdFn on_duplicate_items);
protected:
static OwningAssetCatalogMap copy_catalog_map(const OwningAssetCatalogMap &orig);
};
} // namespace blender::asset_system

View File

@@ -0,0 +1,298 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup asset_system
*/
#include <iostream>
#include "BLI_fileops.hh"
#include "BLI_path_utils.hh"
#include "CLG_log.h"
#include "asset_catalog_definition_file.hh"
namespace blender {
static CLG_LogRef LOG = {"asset.catalog"};
namespace asset_system {
const int AssetCatalogDefinitionFile::SUPPORTED_VERSION = 1;
const std::string AssetCatalogDefinitionFile::VERSION_MARKER = "VERSION ";
const std::string AssetCatalogDefinitionFile::HEADER =
"# This is an Asset Catalog Definition file for Blender.\n"
"#\n"
"# Empty lines and lines starting with `#` will be ignored.\n"
"# The first non-ignored line should be the version indicator.\n"
"# Other lines are of the format \"UUID:catalog/path/for/assets:simple catalog name\"\n";
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::add_overwrite(AssetCatalog *catalog)
{
catalogs_.add_overwrite(catalog->catalog_id, catalog);
}
void AssetCatalogDefinitionFile::forget(CatalogID catalog_id)
{
catalogs_.remove(catalog_id);
}
void AssetCatalogDefinitionFile::parse_catalog_file(
const CatalogFilePath &catalog_definition_file_path,
AssetCatalogParsedFn catalog_loaded_callback)
{
fstream infile(catalog_definition_file_path, std::ios::in);
if (!infile.is_open()) {
CLOG_ERROR(&LOG, "%s: unable to open file", catalog_definition_file_path.c_str());
return;
}
bool seen_version_number = false;
std::string line;
while (std::getline(infile, line)) {
const StringRef trimmed_line = StringRef(line).trim();
if (trimmed_line.is_empty() || trimmed_line[0] == '#') {
continue;
}
if (!seen_version_number) {
/* The very first non-ignored line should be the version declaration. */
const bool is_valid_version = this->parse_version_line(trimmed_line);
if (!is_valid_version) {
std::cerr << catalog_definition_file_path
<< ": first line should be version declaration; ignoring file." << std::endl;
break;
}
seen_version_number = true;
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;
}
/* The AssetDefinitionFile should include this catalog when writing it back to disk. */
this->add_overwrite(non_owning_ptr);
}
}
bool AssetCatalogDefinitionFile::parse_version_line(const StringRef line)
{
if (!line.startswith(VERSION_MARKER)) {
return false;
}
const std::string version_string = line.substr(VERSION_MARKER.length());
const int file_version = std::atoi(version_string.c_str());
/* No versioning, just a blunt check whether it's the right one. */
return file_version == SUPPORTED_VERSION;
}
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 catalog 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);
std::string path_in_file;
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". */
path_in_file = path_and_simple_name;
simple_name = "";
}
else {
path_in_file = path_and_simple_name.substr(0, second_delim);
simple_name = path_and_simple_name.substr(second_delim + 1).trim();
}
AssetCatalogPath catalog_path = path_in_file;
return std::make_unique<AssetCatalog>(catalog_id, catalog_path.cleanup(), simple_name);
}
AssetCatalogDefinitionFile::AssetCatalogDefinitionFile(const CatalogFilePath &file_path)
: file_path(file_path)
{
}
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_overwrite(dest_file_path.c_str(), backup_path.c_str())) {
/* TODO: communicate what went wrong. */
return false;
}
}
if (BLI_rename_overwrite(writable_path.c_str(), dest_file_path.c_str())) {
/* TODO: communicate what went wrong. */
return false;
}
return true;
}
bool AssetCatalogDefinitionFile::exists_on_disk() const
{
return BLI_exists(this->file_path.c_str());
}
bool AssetCatalogDefinitionFile::write_to_disk_unsafe(const CatalogFilePath &dest_file_path) const
{
char directory[PATH_MAX];
BLI_path_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;
}
fstream output(dest_file_path, std::ios::out);
/* TODO(@sybren): remember the line ending style that was originally read, then use that to write
* the file again. */
/* Write the header. */
output << HEADER;
output << "" << std::endl;
output << VERSION_MARKER << SUPPORTED_VERSION << std::endl;
output << "" << std::endl;
/* Write the catalogs, ordered by path (primary) and UUID (secondary). */
AssetCatalogOrderedSet catalogs_by_path;
for (const AssetCatalog *catalog : catalogs_.values()) {
if (catalog->flags.is_deleted) {
continue;
}
catalogs_by_path.insert(catalog);
}
for (const AssetCatalog *catalog : catalogs_by_path) {
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;
}
std::unique_ptr<AssetCatalogDefinitionFile> AssetCatalogDefinitionFile::copy_and_remap(
const OwningAssetCatalogMap &catalogs, const OwningAssetCatalogMap &deleted_catalogs) const
{
auto copy = std::make_unique<AssetCatalogDefinitionFile>(*this);
copy->catalogs_.clear();
/* Remap pointers of the copy from the original AssetCatalogCollection to the given one. */
for (CatalogID catalog_id : catalogs_.keys()) {
/* The catalog can be in the regular or the deleted map. */
const std::unique_ptr<AssetCatalog> *remapped_catalog_uptr_ptr = catalogs.lookup_ptr(
catalog_id);
if (remapped_catalog_uptr_ptr) {
copy->catalogs_.add_new(catalog_id, remapped_catalog_uptr_ptr->get());
continue;
}
remapped_catalog_uptr_ptr = deleted_catalogs.lookup_ptr(catalog_id);
if (remapped_catalog_uptr_ptr) {
copy->catalogs_.add_new(catalog_id, remapped_catalog_uptr_ptr->get());
continue;
}
BLI_assert_msg(false, "A CDF should only reference known catalogs.");
}
return copy;
}
} // namespace asset_system
} // namespace blender

View File

@@ -0,0 +1,91 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup asset_system
*
* Classes internal to the asset system for asset catalog management.
*/
#pragma once
#include "AS_asset_catalog.hh"
#include "BLI_string_ref.hh"
namespace blender::asset_system {
/**
* 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 {
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_;
public:
/* For now this is the only version of the catalog definition files that is supported.
* Later versioning code may be added to handle older files. */
const static int SUPPORTED_VERSION;
/* String that's matched in the catalog definition file to know that the line is the version
* declaration. It has to start with a space to ensure it won't match any hypothetical future
* field that starts with "VERSION". */
const static std::string VERSION_MARKER;
const static std::string HEADER;
const CatalogFilePath file_path;
AssetCatalogDefinitionFile(const CatalogFilePath &file_path);
/**
* 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;
/**
* Returns whether this file exists on disk.
*/
bool exists_on_disk() const;
bool contains(CatalogID catalog_id) const;
/** Add a catalog, overwriting the one with the same catalog ID. */
void add_overwrite(AssetCatalog *catalog);
/** Add a new catalog. Undefined behavior if a catalog with the same ID was already added. */
void add_new(AssetCatalog *catalog);
/** Remove the catalog from the collection of catalogs stored in this file. */
void forget(CatalogID catalog_id);
using AssetCatalogParsedFn = FunctionRef<bool(std::unique_ptr<AssetCatalog>)>;
void parse_catalog_file(const CatalogFilePath &catalog_definition_file_path,
AssetCatalogParsedFn catalog_loaded_callback);
std::unique_ptr<AssetCatalogDefinitionFile> copy_and_remap(
const OwningAssetCatalogMap &catalogs, const OwningAssetCatalogMap &deleted_catalogs) const;
protected:
bool parse_version_line(StringRef line);
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;
};
} // namespace blender::asset_system

View File

@@ -0,0 +1,228 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup asset_system
*/
#include "AS_asset_catalog_path.hh"
#include "BLI_path_utils.hh"
#include <sstream>
namespace blender::asset_system {
const char AssetCatalogPath::SEPARATOR = '/';
AssetCatalogPath::AssetCatalogPath(std::string path) : path_(std::move(path)) {}
AssetCatalogPath::AssetCatalogPath(StringRef path) : path_(path) {}
AssetCatalogPath::AssetCatalogPath(const char *path) : path_(path) {}
AssetCatalogPath::AssetCatalogPath(AssetCatalogPath &&other_path) noexcept
: path_(std::move(other_path.path_))
{
}
uint64_t AssetCatalogPath::hash() const
{
std::hash<std::string> hasher{};
return hasher(path_);
}
uint64_t AssetCatalogPath::length() const
{
return path_.length();
}
const char *AssetCatalogPath::c_str() const
{
return path_.c_str();
}
const std::string &AssetCatalogPath::str() const
{
return path_;
}
StringRefNull AssetCatalogPath::name() const
{
const size_t last_sep_index = path_.rfind(SEPARATOR);
if (last_sep_index == std::string::npos) {
return StringRefNull(path_);
}
return StringRefNull(path_.c_str() + last_sep_index + 1);
}
bool AssetCatalogPath::operator==(const AssetCatalogPath &other_path) const
{
return path_ == other_path.path_;
}
bool AssetCatalogPath::operator!=(const AssetCatalogPath &other_path) const
{
return !(*this == other_path);
}
bool AssetCatalogPath::operator<(const AssetCatalogPath &other_path) const
{
return path_ < other_path.path_;
}
AssetCatalogPath AssetCatalogPath::operator/(const AssetCatalogPath &path_to_append) const
{
/* `"" / "path"` or `"path" / ""` should just result in `"path"` */
if (!*this) {
return path_to_append;
}
if (!path_to_append) {
return *this;
}
std::stringstream new_path;
new_path << path_ << SEPARATOR << path_to_append.path_;
return AssetCatalogPath(new_path.str());
}
AssetCatalogPath::operator bool() const
{
return !path_.empty();
}
std::ostream &operator<<(std::ostream &stream, const AssetCatalogPath &path_to_append)
{
stream << path_to_append.path_;
return stream;
}
AssetCatalogPath AssetCatalogPath::from_user_input(const char *path)
{
return AssetCatalogPath(path).cleanup();
}
AssetCatalogPath AssetCatalogPath::cleanup() const
{
std::stringstream clean_components;
bool first_component_seen = false;
this->iterate_components([&clean_components, &first_component_seen](StringRef component_name,
bool /*is_last_component*/) {
const std::string clean_component = cleanup_component(component_name);
if (clean_component.empty()) {
/* These are caused by leading, trailing, or double slashes. */
return;
}
/* If a previous path component has been streamed already, we need a path separator. This
* cannot use the `is_last_component` boolean, because the last component might be skipped due
* to the condition above. */
if (first_component_seen) {
clean_components << SEPARATOR;
}
first_component_seen = true;
clean_components << clean_component;
});
return AssetCatalogPath(clean_components.str());
}
std::string AssetCatalogPath::cleanup_component(StringRef component_name)
{
std::string cleaned = component_name.trim();
/* Replace colons with something else, as those are used in the CDF file as delimiter. */
std::replace(cleaned.begin(), cleaned.end(), ':', '-');
return cleaned;
}
bool AssetCatalogPath::is_contained_in(const AssetCatalogPath &other_path) const
{
if (!other_path) {
/* The empty path contains all other paths. */
return true;
}
if (path_ == other_path.path_) {
/* Weak is-in relation: equal paths contain each other. */
return true;
}
/* To be a child path of 'other_path', our path must be at least a separator and another
* character longer. */
if (this->length() < other_path.length() + 2) {
return false;
}
/* Create StringRef to be able to use .startswith(). */
const StringRef this_path(path_);
const bool prefix_ok = this_path.startswith(other_path.path_);
const char next_char = this_path[other_path.length()];
return prefix_ok && next_char == SEPARATOR;
}
AssetCatalogPath AssetCatalogPath::parent() const
{
if (!*this) {
return AssetCatalogPath("");
}
std::string::size_type last_sep_index = path_.rfind(SEPARATOR);
if (last_sep_index == std::string::npos) {
return AssetCatalogPath("");
}
return AssetCatalogPath(path_.substr(0, last_sep_index));
}
void AssetCatalogPath::iterate_components(ComponentIteratorFn callback) const
{
const char *next_slash_ptr;
for (const char *path_component = path_.data(); path_component && path_component[0];
/* Jump to one after the next slash if there is any. */
path_component = next_slash_ptr ? next_slash_ptr + 1 : nullptr)
{
/* Note that this also treats backslashes as component separators, which
* helps in cleaning up backslash-separated paths. */
next_slash_ptr = BLI_path_slash_find(path_component);
const bool is_last_component = next_slash_ptr == nullptr;
/* Note that this won't be null terminated. */
const StringRef component_name = is_last_component ?
path_component :
StringRef(path_component,
next_slash_ptr - path_component);
callback(component_name, is_last_component);
}
}
AssetCatalogPath AssetCatalogPath::rebase(const AssetCatalogPath &from_path,
const AssetCatalogPath &to_path) const
{
if (!from_path) {
if (!to_path) {
return AssetCatalogPath("");
}
return to_path / *this;
}
if (!this->is_contained_in(from_path)) {
return AssetCatalogPath("");
}
if (*this == from_path) {
/* Early return, because otherwise the length+1 below is going to cause problems. */
return to_path;
}
/* When from_path = "test", we need to skip "test/" to get the rest of the path, hence the +1. */
const StringRef suffix = StringRef(path_).substr(from_path.length() + 1);
const AssetCatalogPath path_suffix(suffix);
return to_path / path_suffix;
}
} // namespace blender::asset_system

View File

@@ -0,0 +1,182 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup asset_system
*/
#include "AS_asset_catalog_tree.hh"
namespace blender::asset_system {
AssetCatalogTreeItem::AssetCatalogTreeItem(StringRef name,
CatalogID catalog_id,
StringRef simple_name,
const AssetCatalogTreeItem *parent)
: name_(name), catalog_id_(catalog_id), simple_name_(simple_name), parent_(parent)
{
}
CatalogID AssetCatalogTreeItem::get_catalog_id() const
{
return catalog_id_;
}
StringRefNull AssetCatalogTreeItem::get_name() const
{
return name_;
}
StringRefNull AssetCatalogTreeItem::get_simple_name() const
{
return simple_name_;
}
bool AssetCatalogTreeItem::has_unsaved_changes() const
{
return has_unsaved_changes_;
}
AssetCatalogPath AssetCatalogTreeItem::catalog_path() const
{
AssetCatalogPath current_path = name_;
for (const AssetCatalogTreeItem *parent = parent_; parent; parent = parent->parent_) {
current_path = AssetCatalogPath(parent->name_) / 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;
}
bool AssetCatalogTreeItem::has_children() const
{
return !children_.empty();
}
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);
}
}
void AssetCatalogTreeItem::foreach_child(const ItemIterFn callback) const
{
for (const auto &[key, item] : children_) {
callback(item);
}
}
void AssetCatalogTreeItem::foreach_item(const ItemIterFn callback) const
{
AssetCatalogTreeItem::foreach_item_recursive(children_, callback);
}
/* ---------------------------------------------------------------------- */
void AssetCatalogTree::insert_item(const AssetCatalog &catalog,
const std::optional<StringRef> skip_prefix)
{
const AssetCatalogTreeItem *parent = nullptr;
/* The children for the currently iterated component, where the following component should be
* added to (if not there yet). */
AssetCatalogTreeItem::ChildMap *current_item_children = &root_items_;
BLI_assert_msg(!ELEM(catalog.path.str()[0], '/', '\\'),
"Malformed catalog path; should not start with a separator");
const CatalogID nil_id{};
std::optional<StringRef> skip_prefix_tmp = skip_prefix;
catalog.path.iterate_components([&](StringRef component_name, const bool is_last_component) {
if (skip_prefix_tmp && skip_prefix_tmp->startswith(component_name)) {
if (skip_prefix_tmp->size() == component_name.size() ||
(*skip_prefix)[component_name.size()] == AssetCatalogPath::SEPARATOR)
{
skip_prefix_tmp = skip_prefix_tmp->drop_prefix(component_name.size() + 1);
return;
}
}
/* Insert new tree element - if no matching one is there yet! */
auto [key_and_item, was_inserted] = current_item_children->emplace(
component_name,
AssetCatalogTreeItem(component_name,
is_last_component ? catalog.catalog_id : nil_id,
is_last_component ? catalog.simple_name : "",
parent));
AssetCatalogTreeItem &item = key_and_item->second;
/* If full path of this catalog already exists as parent path of a previously read catalog,
* we can ensure this tree item's UUID is set here. */
if (is_last_component) {
if (BLI_uuid_is_nil(item.catalog_id_) || catalog.flags.is_first_loaded) {
item.catalog_id_ = catalog.catalog_id;
}
item.has_unsaved_changes_ = catalog.flags.has_unsaved_changes;
}
/* Walk further into the path (no matter if a new item was created or not). */
parent = &item;
current_item_children = &item.children_;
});
}
void AssetCatalogTree::foreach_item(AssetCatalogTreeItem::ItemIterFn callback) const
{
AssetCatalogTreeItem::foreach_item_recursive(root_items_, callback);
}
void AssetCatalogTree::foreach_root_item(const ItemIterFn callback) const
{
for (const auto &[key, item] : root_items_) {
callback(item);
}
}
bool AssetCatalogTree::is_empty() const
{
return root_items_.empty();
}
const AssetCatalogTreeItem *AssetCatalogTree::find_item(const AssetCatalogPath &path) const
{
const AssetCatalogTreeItem *result = nullptr;
this->foreach_item([&](const AssetCatalogTreeItem &item) {
if (result) {
/* There is no way to stop iteration. */
return;
}
if (item.catalog_path() == path) {
result = &item;
}
});
return result;
}
const AssetCatalogTreeItem *AssetCatalogTree::find_root_item(const AssetCatalogPath &path) const
{
const AssetCatalogTreeItem *result = nullptr;
this->foreach_root_item([&](const AssetCatalogTreeItem &item) {
if (result) {
/* There is no way to stop iteration. */
return;
}
if (item.catalog_path() == path) {
result = &item;
}
});
return result;
}
} // namespace blender::asset_system

View File

@@ -0,0 +1,615 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup asset_system
*/
#include <memory>
#include "AS_asset_catalog.hh"
#include "AS_asset_library.hh"
#include "AS_asset_representation.hh"
#include "AS_essentials_library.hh"
#include "AS_remote_library.hh"
#include "BKE_lib_remap.hh"
#include "BKE_main.hh"
#include "BKE_preferences.h"
#include "BLI_listbase.h" // IWYU pragma: keep
#include "BLI_path_utils.hh"
#include "BLI_string.h"
#include "DNA_asset_types.h"
#include "DNA_space_types.h"
#include "DNA_userdef_types.h"
#include "DNA_windowmanager_types.h"
#include "asset_catalog_collection.hh"
#include "asset_library_service.hh"
#include "runtime_library.hh"
#include "utils.hh"
namespace blender {
using namespace blender::asset_system;
bool AssetLibrary::save_catalogs_when_file_is_saved = true;
void AS_asset_libraries_exit()
{
/* NOTE: Can probably removed once #WITH_DESTROY_VIA_LOAD_HANDLER gets enabled by default. */
AssetLibraryService::destroy();
}
AssetLibrary *AS_asset_library_load(const Main *bmain,
const AssetLibraryReference &library_reference)
{
AssetLibraryService *service = AssetLibraryService::get();
return service->get_asset_library(bmain, library_reference);
}
AssetLibrary *AS_asset_library_load_from_directory(const char *name, const char *library_dirpath)
{
/* NOTE: Loading an asset library at this point only means loading the catalogs.
* Later on this should invoke reading of asset representations too. */
AssetLibraryService *service = AssetLibraryService::get();
AssetLibrary *lib;
if (library_dirpath == nullptr || library_dirpath[0] == '\0') {
lib = service->get_asset_library_current_file();
}
else {
lib = service->get_asset_library_on_disk_custom(name, library_dirpath);
}
return lib;
}
bool AS_asset_library_has_any_unsaved_catalogs()
{
AssetLibraryService *service = AssetLibraryService::get();
return service->has_any_unsaved_catalogs();
}
std::string AS_asset_library_root_path_from_library_ref(
const AssetLibraryReference &library_reference)
{
return AssetLibraryService::root_path_from_library_ref(library_reference);
}
std::string AS_asset_library_find_suitable_root_path_from_path(const StringRefNull input_path)
{
if (bUserAssetLibrary *preferences_lib = BKE_preferences_asset_library_containing_path(
&U, input_path.c_str()))
{
return preferences_lib->dirpath;
}
char buffer[FILE_MAXDIR];
BLI_path_split_dir_part(input_path.c_str(), buffer, FILE_MAXDIR);
return buffer;
}
std::string AS_asset_library_find_suitable_root_path_from_main(const Main *bmain)
{
return AS_asset_library_find_suitable_root_path_from_path(bmain->filepath);
}
void AS_asset_library_remap_ids(const bke::id::IDRemapper &mappings)
{
AssetLibraryService *service = AssetLibraryService::get();
service->foreach_loaded_asset_library(
[mappings](AssetLibrary &library) { library.remap_ids_and_remove_invalid(mappings); }, true);
}
void AS_asset_full_path_explode_from_weak_ref(const AssetWeakReference *asset_reference,
char r_path_buffer[/*FILE_MAX_LIBEXTRA*/ 1282],
char **r_dir,
char **r_group,
char **r_name)
{
AssetLibraryService *service = AssetLibraryService::get();
std::optional<AssetLibraryService::ExplodedPath> exploded =
service->resolve_asset_weak_reference_to_exploded_path(*asset_reference);
if (!exploded) {
if (r_dir) {
*r_dir = nullptr;
}
if (r_group) {
*r_group = nullptr;
}
if (r_name) {
*r_name = nullptr;
}
r_path_buffer[0] = '\0';
return;
}
BLI_assert(!exploded->group_component.is_empty());
BLI_assert(!exploded->name_component.is_empty());
BLI_strncpy(r_path_buffer, exploded->full_path->c_str(), /*FILE_MAX_LIBEXTRA*/ 1282);
if (!exploded->dir_component.is_empty()) {
r_path_buffer[exploded->dir_component.size()] = '\0';
r_path_buffer[exploded->dir_component.size() + 1 + exploded->group_component.size()] = '\0';
if (r_dir) {
*r_dir = r_path_buffer;
}
if (r_group) {
*r_group = r_path_buffer + exploded->dir_component.size() + 1;
}
if (r_name) {
*r_name = r_path_buffer + exploded->dir_component.size() + 1 +
exploded->group_component.size() + 1;
}
}
else {
r_path_buffer[exploded->group_component.size()] = '\0';
if (r_dir) {
*r_dir = nullptr;
}
if (r_group) {
*r_group = r_path_buffer;
}
if (r_name) {
*r_name = r_path_buffer + exploded->group_component.size() + 1;
}
}
}
static void update_import_method_for_user_libraries()
{
for (bUserAssetLibrary &library : U.asset_libraries) {
if (U.experimental.no_data_block_packing) {
if (library.import_method == ASSET_IMPORT_PACK) {
library.import_method = ASSET_IMPORT_APPEND_REUSE;
}
}
else {
if (library.import_method == ASSET_IMPORT_APPEND_REUSE) {
library.import_method = ASSET_IMPORT_PACK;
}
}
}
}
static void update_import_method_for_asset_browsers(Main &bmain)
{
for (bScreen &screen : bmain.screens) {
for (ScrArea &area : screen.areabase) {
for (SpaceLink &sl : area.spacedata) {
if (sl.spacetype != SPACE_FILE) {
continue;
}
SpaceFile *sfile = reinterpret_cast<SpaceFile *>(&sl);
if (!sfile->asset_params) {
continue;
}
if (U.experimental.no_data_block_packing) {
if (sfile->asset_params->import_method == FILE_ASSET_IMPORT_PACK) {
sfile->asset_params->import_method = FILE_ASSET_IMPORT_APPEND_REUSE;
}
}
else {
if (sfile->asset_params->import_method == FILE_ASSET_IMPORT_APPEND_REUSE) {
sfile->asset_params->import_method = FILE_ASSET_IMPORT_PACK;
}
}
}
}
}
}
void AS_asset_library_import_method_ensure_valid(Main &bmain)
{
update_import_method_for_user_libraries();
update_import_method_for_asset_browsers(bmain);
}
namespace asset_system {
AssetLibrary::AssetLibrary(eAssetLibraryType library_type,
const bool is_read_only,
StringRef name,
StringRef root_path)
: library_type_(library_type),
is_read_only_(is_read_only),
name_(name),
root_path_(std::make_shared<std::string>(utils::normalize_directory_path(root_path))),
catalog_service_(std::make_unique<AssetCatalogService>(
*root_path_,
is_read_only ? std::optional{AssetCatalogService::read_only_tag{}} : std::nullopt))
{
}
AssetLibrary::~AssetLibrary()
{
if (on_save_callback_store_.func) {
this->on_blend_save_handler_unregister();
}
}
void AssetLibrary::foreach_loaded(FunctionRef<void(AssetLibrary &)> fn,
const bool include_all_library)
{
AssetLibraryService *service = AssetLibraryService::get();
service->foreach_loaded_asset_library(fn, include_all_library);
}
void AssetLibrary::force_remote_listing_download() const
{
/* Default implementation is a no-op. */
}
bool AssetLibrary::use_relative_paths() const
{
return true;
}
std::optional<StringRefNull> AssetLibrary::remote_url() const
{
/* Remote asset library support is implemented in #RemoteAssetLibrary::remote_url(). */
return {};
};
AssetCatalogService &AssetLibrary::catalog_service() const
{
std::lock_guard lock{catalog_service_mutex_};
return *catalog_service_;
}
std::shared_ptr<AssetCatalogService> AssetLibrary::catalog_service_ptr() const
{
std::lock_guard lock{catalog_service_mutex_};
return catalog_service_;
}
void AssetLibrary::refresh_catalogs()
{
/* To be implemented by a subclass, like #OnDiskAssetLibrary::refresh_catalogs. */
}
void AssetLibrary::load_or_reload_catalogs()
{
std::lock_guard lock{catalog_service_mutex_};
/* Should never actually be the case, catalog service gets allocated with the asset library. */
if (catalog_service_ == nullptr) {
auto catalog_service = std::make_unique<AssetCatalogService>(*root_path_);
catalog_service->load_from_disk();
catalog_service_ = std::move(catalog_service);
return;
}
/* The catalog service was created before without being associated with a definition file. */
if (catalog_service_->get_catalog_definition_file() == nullptr) {
catalog_service_->load_from_disk();
if (library_type() == ASSET_LIBRARY_ESSENTIALS) {
this->refresh_catalogs();
}
}
else {
this->refresh_catalogs();
}
}
std::weak_ptr<AssetRepresentation> AssetLibrary::add_external_on_disk_asset(
StringRef relative_asset_path,
StringRef name,
const int id_type,
std::unique_ptr<AssetMetaData> metadata)
{
std::scoped_lock lock{asset_storage_.external_assets_mutex};
return asset_storage_.external_assets.lookup_key_or_add(std::make_shared<AssetRepresentation>(
relative_asset_path, name, id_type, std::move(metadata), *this));
}
std::weak_ptr<AssetRepresentation> AssetLibrary::add_external_online_asset(
StringRef relative_asset_path,
StringRef name,
const int id_type,
std::unique_ptr<AssetMetaData> metadata,
OnlineAssetInfo online_info)
{
std::scoped_lock lock{asset_storage_.external_assets_mutex};
return asset_storage_.external_assets.lookup_key_or_add(std::make_shared<AssetRepresentation>(
relative_asset_path, name, id_type, std::move(metadata), *this, online_info));
}
std::weak_ptr<AssetRepresentation> AssetLibrary::add_local_id_asset(ID &id)
{
std::scoped_lock lock{asset_storage_.local_id_assets_mutex};
return asset_storage_.local_id_assets.lookup_key_or_add(
std::make_shared<AssetRepresentation>(id, *this));
}
bool AssetLibrary::remove_asset(AssetRepresentation &asset)
{
/* Make sure this is forwarded to the library actually owning the asset if needed. For example
* the "All Libraries" library doesn't own the assets itself. */
if (&asset.owner_asset_library_ != this) {
return asset.owner_asset_library_.remove_asset(asset);
}
std::scoped_lock lock{asset_storage_.external_assets_mutex,
asset_storage_.local_id_assets_mutex};
BLI_assert(asset_storage_.local_id_assets.contains_as(&asset) ||
asset_storage_.external_assets.contains_as(&asset));
if (asset_storage_.local_id_assets.remove_as(&asset)) {
return true;
}
return asset_storage_.external_assets.remove_as(&asset);
}
void AssetLibrary::remap_ids_and_remove_invalid(const bke::id::IDRemapper &mappings)
{
Set<AssetRepresentation *> removed_assets;
{
std::scoped_lock lock{asset_storage_.local_id_assets_mutex};
for (const auto &asset_ptr : asset_storage_.local_id_assets) {
AssetRepresentation &asset = *asset_ptr;
BLI_assert(asset.is_local_id());
const IDRemapperApplyResult result = mappings.apply(&std::get<ID *>(asset.asset_),
ID_REMAP_APPLY_DEFAULT);
/* Entirely remove assets whose ID is unset. We don't want assets with a null ID pointer. */
if (result == ID_REMAP_RESULT_SOURCE_UNASSIGNED) {
removed_assets.add(&asset);
}
}
}
for (AssetRepresentation *asset : removed_assets) {
this->remove_asset(*asset);
}
}
namespace {
void asset_library_on_save_post(Main *bmain,
PointerRNA **pointers,
const int num_pointers,
void *arg)
{
AssetLibrary *asset_lib = static_cast<AssetLibrary *>(arg);
/* Transform 'runtime' current file library into 'on-disk' current file library. */
if (asset_lib->library_type() == ASSET_LIBRARY_LOCAL && asset_lib->root_path().is_empty()) {
BLI_assert(dynamic_cast<RuntimeAssetLibrary *>(asset_lib) != nullptr);
if (AssetLibrary *on_disk_lib =
AssetLibraryService::move_runtime_current_file_into_on_disk_library(*bmain))
{
/* Allow undoing to the state before merging in catalogs from disk. */
on_disk_lib->catalog_service().undo_push();
/* Force refresh to merge on-disk catalogs with the ones stolen from the runtime library. */
asset_lib = AssetLibraryService::get()->get_asset_library_on_disk_builtin(
ASSET_LIBRARY_LOCAL, on_disk_lib->root_path());
BLI_assert(asset_lib == on_disk_lib);
}
}
asset_lib->on_blend_save_post(bmain, pointers, num_pointers);
}
} // namespace
void AssetLibrary::on_blend_save_handler_register()
{
/* The callback system doesn't own `on_save_callback_store_`. */
on_save_callback_store_.alloc = false;
on_save_callback_store_.func = asset_library_on_save_post;
on_save_callback_store_.arg = this;
BKE_callback_add(&on_save_callback_store_, BKE_CB_EVT_SAVE_POST);
}
void AssetLibrary::on_blend_save_handler_unregister()
{
BKE_callback_remove(&on_save_callback_store_, BKE_CB_EVT_SAVE_POST);
on_save_callback_store_.func = nullptr;
on_save_callback_store_.arg = nullptr;
}
void AssetLibrary::on_blend_save_post(Main *bmain,
PointerRNA ** /*pointers*/,
const int /*num_pointers*/)
{
if (save_catalogs_when_file_is_saved && !this->catalog_service().is_read_only()) {
this->catalog_service().write_to_disk(bmain->filepath);
}
}
std::string AssetLibrary::resolve_asset_weak_reference_to_full_path(
const AssetWeakReference &asset_reference)
{
AssetLibraryService *service = AssetLibraryService::get();
return service->resolve_asset_weak_reference_to_full_path(asset_reference);
}
void AssetLibrary::refresh_catalog_simplename(AssetMetaData *asset_data)
{
if (BLI_uuid_is_nil(asset_data->catalog_id)) {
asset_data->catalog_simple_name[0] = '\0';
return;
}
const AssetCatalog *catalog = this->catalog_service().find_catalog(asset_data->catalog_id);
if (catalog == nullptr) {
/* No-op if the catalog cannot be found. This could be the kind of "the catalog definition file
* is corrupt/lost" scenario that the simple name is meant to help recover from. */
return;
}
STRNCPY(asset_data->catalog_simple_name, catalog->simple_name.c_str());
}
eAssetLibraryType AssetLibrary::library_type() const
{
return library_type_;
}
StringRefNull AssetLibrary::name() const
{
return name_;
}
StringRefNull AssetLibrary::root_path() const
{
return *root_path_;
}
bool AssetLibrary::is_read_only() const
{
return is_read_only_;
}
Vector<AssetLibraryReference> all_valid_asset_library_refs()
{
Vector<AssetLibraryReference> result;
{
AssetLibraryReference library_ref{};
library_ref.custom_library_index = -1;
library_ref.type = ASSET_LIBRARY_ESSENTIALS;
result.append(library_ref);
}
const bool include_remote_libraries = USER_EXPERIMENTAL_TEST(&U, use_remote_asset_libraries);
const bool include_online_essentials = (U.asset_flag & USER_ASSETS_USE_ONLINE_ESSENTIALS) != 0;
if (include_remote_libraries && include_online_essentials) {
AssetLibraryReference library_ref{};
library_ref.custom_library_index = -1;
library_ref.type = ASSET_LIBRARY_ONLINE_ESSENTIALS;
result.append(library_ref);
}
for (const auto [i, asset_library] : U.asset_libraries.enumerate()) {
if (!BKE_preferences_asset_library_is_valid(&U, &asset_library, true)) {
continue;
}
AssetLibraryReference library_ref{};
library_ref.custom_library_index = i;
library_ref.type = ASSET_LIBRARY_CUSTOM;
result.append(library_ref);
}
AssetLibraryReference library_ref{};
library_ref.custom_library_index = -1;
library_ref.type = ASSET_LIBRARY_LOCAL;
result.append(library_ref);
return result;
}
AssetLibraryReference all_library_reference()
{
AssetLibraryReference all_library_ref{};
all_library_ref.custom_library_index = -1;
all_library_ref.type = ASSET_LIBRARY_ALL;
return all_library_ref;
}
AssetLibraryReference essentials_library_reference()
{
AssetLibraryReference all_library_ref{};
all_library_ref.custom_library_index = -1;
all_library_ref.type = ASSET_LIBRARY_ESSENTIALS;
return all_library_ref;
}
AssetLibraryReference current_file_library_reference()
{
AssetLibraryReference library_ref{};
library_ref.custom_library_index = -1;
library_ref.type = ASSET_LIBRARY_LOCAL;
return library_ref;
}
AssetLibraryReference online_essentials_library_reference()
{
AssetLibraryReference library_ref{};
library_ref.custom_library_index = -1;
library_ref.type = ASSET_LIBRARY_ONLINE_ESSENTIALS;
return library_ref;
}
void all_library_tag_catalogs_dirty()
{
AssetLibraryService *service = AssetLibraryService::get();
service->tag_all_library_catalogs_dirty();
}
void all_library_reload_catalogs_if_dirty()
{
AssetLibraryService *service = AssetLibraryService::get();
service->reload_all_library_catalogs_if_dirty();
}
bool is_or_contains_remote_libraries(const AssetLibraryReference &reference)
{
switch (reference.type) {
/* Also returns true since it contains the online essentials. */
case ASSET_LIBRARY_ALL:
return true;
case ASSET_LIBRARY_ESSENTIALS:
case ASSET_LIBRARY_ONLINE_ESSENTIALS:
return true;
case ASSET_LIBRARY_CUSTOM: {
if (bUserAssetLibrary *asset_library =
AssetLibraryService::find_custom_asset_library_from_library_ref(reference))
{
if (asset_library->flag & ASSET_LIBRARY_USE_REMOTE_URL) {
return true;
}
}
break;
}
case ASSET_LIBRARY_LOCAL:
return false;
}
return false;
}
bool contains_assets_from_remote_url(const AssetLibrary &library, const StringRef remote_url)
{
switch (library.library_type()) {
case ASSET_LIBRARY_ALL: {
if (is_online_essentials_url(remote_url)) {
return true;
}
bool has_match = false;
AssetLibrary::foreach_loaded(
[&](const AssetLibrary &nested) {
if (nested.remote_url() == remote_url) {
has_match = true;
}
},
/*include_all_library=*/false);
return has_match;
}
case ASSET_LIBRARY_ESSENTIALS:
case ASSET_LIBRARY_ONLINE_ESSENTIALS:
return is_online_essentials_url(remote_url);
case ASSET_LIBRARY_CUSTOM:
return library.remote_url() == remote_url;
case ASSET_LIBRARY_LOCAL:
return false;
}
return false;
}
} // namespace asset_system
} // namespace blender

View File

@@ -0,0 +1,763 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup asset_system
*/
#include <memory>
#include "BKE_blender.hh"
#include "BKE_preferences.h"
#include "BLI_fileops.h" // IWYU pragma: keep
#include "BLI_path_utils.hh"
#include "BLI_string_ref.hh"
#include "BLI_vector.hh"
#include "DNA_asset_types.h"
#include "DNA_userdef_types.h"
#include "CLG_log.h"
#include "AS_asset_library.hh"
#include "AS_essentials_library.hh"
#include "all_library.hh"
#include "asset_catalog_collection.hh"
#include "asset_catalog_definition_file.hh" // IWYU pragma: keep
#include "asset_library_service.hh"
#include "essentials_library.hh"
#include "on_disk_library.hh"
#include "preferences_on_disk_library.hh"
#include "remote_library.hh"
#include "runtime_library.hh"
#include "utils.hh"
namespace blender {
/* When enabled, use a pre file load handler (#BKE_CB_EVT_LOAD_PRE) callback to destroy the asset
* library service. Without this an explicit call from the file loading code is needed to do this,
* which is not as nice.
*
* TODO Currently disabled because UI data depends on asset library data, so we have to make sure
* it's freed in the right order (UI first). Pre-load handlers don't give us this order.
* Should be addressed with a proper ownership model for the asset system:
* https://developer.blender.org/docs/features/asset_system/backend/#ownership-model
*/
// #define WITH_DESTROY_VIA_LOAD_HANDLER
static CLG_LogRef LOG = {"asset.library"};
namespace asset_system {
std::unique_ptr<AssetLibraryService> AssetLibraryService::instance_;
bool AssetLibraryService::atexit_handler_registered_ = false;
AssetLibraryService *AssetLibraryService::get()
{
if (!instance_) {
allocate_service_instance();
}
return instance_.get();
}
void AssetLibraryService::destroy()
{
if (!instance_) {
return;
}
instance_->app_handler_unregister();
instance_.reset();
}
AssetLibrary *AssetLibraryService::get_asset_library(
const Main *bmain, const AssetLibraryReference &library_reference)
{
const eAssetLibraryType type = eAssetLibraryType(library_reference.type);
switch (type) {
case ASSET_LIBRARY_ESSENTIALS: {
const StringRefNull root_path = essentials_directory_path();
if (root_path.is_empty()) {
return nullptr;
}
return this->get_asset_library_on_disk_builtin(type, root_path);
}
case ASSET_LIBRARY_ONLINE_ESSENTIALS: {
return this->get_online_essentials_asset_library();
}
case ASSET_LIBRARY_LOCAL: {
/* For the "Current File" library we get the asset library root path based on main. */
std::string root_path = bmain ? AS_asset_library_find_suitable_root_path_from_main(bmain) :
"";
if (root_path.empty()) {
/* File wasn't saved yet. */
return this->get_asset_library_current_file();
}
return this->get_asset_library_on_disk_builtin(type, root_path);
}
case ASSET_LIBRARY_ALL:
return this->get_asset_library_all(bmain);
case ASSET_LIBRARY_CUSTOM: {
bUserAssetLibrary *custom_library = find_custom_asset_library_from_library_ref(
library_reference);
if (!custom_library) {
return nullptr;
}
if (custom_library->flag & ASSET_LIBRARY_USE_REMOTE_URL) {
if (is_online_essentials_url(custom_library->remote_url)) {
return this->get_online_essentials_asset_library();
}
return this->get_preferences_remote_asset_library(*custom_library);
}
std::string root_path = custom_library->dirpath;
if (root_path.empty()) {
return nullptr;
}
AssetLibrary *library = this->get_asset_library_on_disk_custom_preferences(custom_library);
library->may_override_import_method_ = true;
return library;
}
}
return nullptr;
}
AssetLibrary *AssetLibraryService::get_online_essentials_asset_library()
{
if (online_essentials_library_) {
CLOG_DEBUG(&LOG, "get online essentials lib (cached)");
online_essentials_library_->load_or_reload_catalogs();
}
else {
CLOG_DEBUG(&LOG, "get online essentials lib (loaded)");
online_essentials_library_ = std::make_unique<OnlineEssentialsLibrary>();
}
AssetLibrary *lib = online_essentials_library_.get();
return lib;
}
AssetLibrary *AssetLibraryService::get_preferences_remote_asset_library(
const bUserAssetLibrary &custom_library)
{
if (!custom_library.remote_url[0]) {
return nullptr;
}
const StringRefNull remote_url = custom_library.remote_url;
/* Lock for the entire "lookup and if not found -> create and insert" scope, so no two threads do
* this in parallel and interfere with each other. */
std::scoped_lock lock{remote_libraries_mutex_};
std::unique_ptr<PreferencesRemoteAssetLibrary> *lib_uptr_ptr = remote_libraries_.lookup_ptr(
remote_url);
if (lib_uptr_ptr != nullptr) {
CLOG_DEBUG(&LOG, "get \"%s\" (cached)", remote_url.c_str());
AssetLibrary *lib = lib_uptr_ptr->get();
lib->load_or_reload_catalogs();
return lib;
}
std::unique_ptr<PreferencesRemoteAssetLibrary> lib_uptr =
std::make_unique<PreferencesRemoteAssetLibrary>(custom_library);
AssetLibrary *lib = lib_uptr.get();
lib->load_or_reload_catalogs();
remote_libraries_.add_new(remote_url, std::move(lib_uptr));
CLOG_DEBUG(&LOG, "get \"%s\" (loaded)", remote_url.c_str());
return lib;
}
AssetLibrary *AssetLibraryService::get_asset_library_on_disk(
eAssetLibraryType library_type,
StringRef name,
StringRefNull root_path,
const bool load_catalogs,
bUserAssetLibrary *preferences_library)
{
const std::string normalized_root_path = utils::normalize_directory_path(root_path);
/* Lock for the entire "lookup and if not found -> create and insert" scope, so no two threads do
* this in parallel and interfere with each other. */
std::scoped_lock lock{on_disk_libraries_mutex_};
if (OnDiskAssetLibrary *lib = this->lookup_on_disk_library(library_type, normalized_root_path)) {
CLOG_DEBUG(&LOG, "get \"%s\" (cached)", normalized_root_path.c_str());
if (load_catalogs) {
lib->load_or_reload_catalogs();
}
return lib;
}
std::unique_ptr<OnDiskAssetLibrary> lib_uptr;
switch (library_type) {
case ASSET_LIBRARY_CUSTOM:
if (preferences_library) {
lib_uptr = std::make_unique<PreferencesOnDiskAssetLibrary>(*preferences_library);
}
else {
/* Only used by unit tests. */
lib_uptr = std::make_unique<OnDiskAssetLibrary>(
library_type, name, normalized_root_path, /*is_read_only=*/false);
}
break;
case ASSET_LIBRARY_ESSENTIALS:
lib_uptr = std::make_unique<EssentialsAssetLibrary>();
break;
case ASSET_LIBRARY_LOCAL:
lib_uptr = std::make_unique<OnDiskAssetLibrary>(
library_type, name, normalized_root_path, /*is_read_only=*/false);
break;
default:
lib_uptr = std::make_unique<OnDiskAssetLibrary>(
library_type, name, normalized_root_path, /*is_read_only=*/true);
break;
}
/* Get underlying pointer before moving. */
AssetLibrary *lib = lib_uptr.get();
on_disk_libraries_.add_new({library_type, normalized_root_path}, std::move(lib_uptr));
CLOG_DEBUG(&LOG, "get \"%s\" (loaded)", normalized_root_path.c_str());
if (load_catalogs) {
lib->load_or_reload_catalogs();
}
return lib;
}
AssetLibrary *AssetLibraryService::get_asset_library_on_disk_custom(StringRef name,
StringRefNull root_path)
{
return this->get_asset_library_on_disk(ASSET_LIBRARY_CUSTOM, name, root_path);
}
AssetLibrary *AssetLibraryService::get_asset_library_on_disk_custom_preferences(
bUserAssetLibrary *custom_library)
{
return this->get_asset_library_on_disk(
ASSET_LIBRARY_CUSTOM, custom_library->name, custom_library->dirpath, true, custom_library);
}
AssetLibrary *AssetLibraryService::get_asset_library_on_disk_builtin(eAssetLibraryType type,
StringRefNull root_path)
{
BLI_assert_msg(
type != ASSET_LIBRARY_CUSTOM,
"Use `get_asset_library_on_disk_custom()` for libraries of type `ASSET_LIBRARY_CUSTOM`");
/* Builtin asset libraries don't need a name, the #eAssetLibraryType is enough to identify them
* (and doesn't change, unlike the name). */
return this->get_asset_library_on_disk(type, {}, root_path);
}
AssetLibrary *AssetLibraryService::get_asset_library_current_file()
{
if (current_file_library_) {
CLOG_DEBUG(&LOG, "get current file lib (cached)");
current_file_library_->refresh_catalogs();
}
else {
CLOG_DEBUG(&LOG, "get current file lib (loaded)");
current_file_library_ = std::make_unique<RuntimeAssetLibrary>();
}
AssetLibrary *lib = current_file_library_.get();
return lib;
}
void AssetLibraryService::tag_all_library_catalogs_dirty()
{
if (all_library_) {
all_library_->tag_catalogs_dirty();
}
}
void AssetLibraryService::reload_all_library_catalogs_if_dirty()
{
if (all_library_ && all_library_->is_catalogs_dirty()) {
/* Don't reload catalogs from nested libraries from disk, just reflect their currently known
* state in the "All" library. Loading catalog changes from disk is only done with a
* #AS_asset_library_load()/#AssetLibraryService:get_asset_library() call. */
const bool reload_nested_catalogs = false;
all_library_->rebuild_catalogs_from_nested(reload_nested_catalogs);
}
}
AssetLibrary *AssetLibraryService::move_runtime_current_file_into_on_disk_library(
const Main &bmain)
{
AssetLibraryService &library_service = *AssetLibraryService::get();
const std::string root_path = AS_asset_library_find_suitable_root_path_from_main(&bmain);
if (root_path.empty()) {
return nullptr;
}
#ifndef NDEBUG
{
std::scoped_lock lock{library_service.on_disk_libraries_mutex_};
BLI_assert_msg(!library_service.lookup_on_disk_library(ASSET_LIBRARY_LOCAL, root_path),
"On-disk \"Current File\" asset library shouldn't exist yet, it should only be "
"created now in response to initially saving the file - catalog service "
"will be overridden");
}
#endif
/* Create on disk library without loading catalogs. We'll steal the catalog service from the
* runtime library below. */
AssetLibrary *on_disk_library = library_service.get_asset_library_on_disk(
ASSET_LIBRARY_LOCAL,
{},
root_path,
/*load_catalogs=*/false);
{
/* These should always be completely separate, just sanity check since it would cause a
* deadlock below. */
BLI_assert(on_disk_library != library_service.current_file_library_.get());
std::lock_guard lock_on_disk{on_disk_library->catalog_service_mutex_};
std::lock_guard lock_runtime{library_service.current_file_library_->catalog_service_mutex_};
on_disk_library->catalog_service_.swap(
library_service.current_file_library_->catalog_service_);
}
AssetCatalogService &catalog_service = on_disk_library->catalog_service();
catalog_service.asset_library_root_ = on_disk_library->root_path();
/* The catalogs are not stored on disk, so there should not be any CDF. Otherwise, we'd have to
* remap their stored file-path too (#AssetCatalogDefinitionFile.file_path). */
BLI_assert_msg(catalog_service.get_catalog_definition_file() == nullptr,
"new on-disk library shouldn't have catalog definition files - root path "
"changed, so they would have to be relocated");
/* Create a CDF with the runtime catalogs that on-disk catalogs can be merged into. Only do if
* there's catalogs to write, otherwise we create empty CDFs on disk on every new .blend save. */
if (!catalog_service.catalog_collection_->is_empty()) {
char asset_lib_cdf_path[PATH_MAX];
BLI_path_join(asset_lib_cdf_path,
sizeof(asset_lib_cdf_path),
on_disk_library->root_path().c_str(),
AssetCatalogService::DEFAULT_CATALOG_FILENAME.c_str());
catalog_service.catalog_collection_->catalog_definition_file_ =
catalog_service.construct_cdf_in_memory(asset_lib_cdf_path);
}
library_service.current_file_library_ = nullptr;
return on_disk_library;
}
AssetLibrary *AssetLibraryService::get_asset_library_all(const Main *bmain)
{
/* (Re-)load all other asset libraries. */
for (AssetLibraryReference &library_ref : all_valid_asset_library_refs()) {
/* Skip self :) */
if (library_ref.type == ASSET_LIBRARY_ALL) {
continue;
}
/* Ensure all asset libraries are loaded. */
this->get_asset_library(bmain, library_ref);
}
if (!all_library_) {
CLOG_DEBUG(&LOG, "get all lib (loaded)");
all_library_ = std::make_unique<AllAssetLibrary>();
}
else {
CLOG_DEBUG(&LOG, "get all lib (cached)");
}
/* Don't reload catalogs, they've just been loaded above. */
all_library_->rebuild_catalogs_from_nested(/*reload_nested_catalogs=*/false);
return all_library_.get();
}
OnDiskAssetLibrary *AssetLibraryService::lookup_on_disk_library(eAssetLibraryType library_type,
StringRefNull root_path)
{
BLI_assert_msg(!root_path.is_empty(),
"top level directory must be given for on-disk asset library");
std::string normalized_root_path = utils::normalize_directory_path(root_path);
std::scoped_lock lock{on_disk_libraries_mutex_};
std::unique_ptr<OnDiskAssetLibrary> *lib_uptr_ptr = on_disk_libraries_.lookup_ptr(
{library_type, normalized_root_path});
return lib_uptr_ptr ? lib_uptr_ptr->get() : nullptr;
}
bUserAssetLibrary *AssetLibraryService::find_custom_preferences_asset_library_from_asset_weak_ref(
const AssetWeakReference &asset_reference)
{
if (!ELEM(asset_reference.asset_library_type, ASSET_LIBRARY_CUSTOM)) {
return nullptr;
}
return BKE_preferences_asset_library_find_by_name(&U, asset_reference.asset_library_identifier);
}
AssetLibrary *AssetLibraryService::find_loaded_on_disk_asset_library_from_name(
StringRef name) const
{
std::scoped_lock lock{on_disk_libraries_mutex_};
for (const std::unique_ptr<OnDiskAssetLibrary> &library : on_disk_libraries_.values()) {
if (library->name_ == name) {
return library.get();
}
}
return nullptr;
}
std::string AssetLibraryService::resolve_asset_weak_reference_to_library_path(
const AssetWeakReference &asset_reference)
{
StringRefNull library_dirpath;
switch (eAssetLibraryType(asset_reference.asset_library_type)) {
case ASSET_LIBRARY_CUSTOM: {
bUserAssetLibrary *custom_lib = find_custom_preferences_asset_library_from_asset_weak_ref(
asset_reference);
if (custom_lib) {
library_dirpath = custom_lib->dirpath;
break;
}
/* A bit of an odd-ball, the API supports loading custom libraries from arbitrary paths (used
* by unit tests). So check all loaded on-disk libraries too. */
AssetLibrary *loaded_custom_lib = this->find_loaded_on_disk_asset_library_from_name(
asset_reference.asset_library_identifier);
if (!loaded_custom_lib) {
return "";
}
library_dirpath = *loaded_custom_lib->root_path_;
break;
}
case ASSET_LIBRARY_ESSENTIALS:
library_dirpath = essentials_directory_path();
break;
case ASSET_LIBRARY_ONLINE_ESSENTIALS:
library_dirpath = online_essentials_cache_directory_path();
break;
case ASSET_LIBRARY_LOCAL:
case ASSET_LIBRARY_ALL:
return "";
}
std::string normalized_library_dirpath = utils::normalize_path(library_dirpath);
return normalized_library_dirpath;
}
int64_t AssetLibraryService::rfind_blendfile_extension(StringRef path)
{
const std::vector<StringRefNull> blendfile_extensions = {".blend" SEP_STR,
".blend.gz" SEP_STR,
".ble" SEP_STR,
".blend" ALTSEP_STR,
".blend.gz" ALTSEP_STR,
".ble" ALTSEP_STR};
int64_t blendfile_extension_pos = StringRef::not_found;
for (StringRefNull blendfile_ext : blendfile_extensions) {
const int64_t iter_ext_pos = path.rfind(blendfile_ext);
if (iter_ext_pos == StringRef::not_found) {
continue;
}
if ((blendfile_extension_pos == StringRef::not_found) ||
(blendfile_extension_pos < iter_ext_pos))
{
blendfile_extension_pos = iter_ext_pos;
}
}
return blendfile_extension_pos;
}
std::string AssetLibraryService::normalize_asset_weak_reference_relative_asset_identifier(
const AssetWeakReference &asset_reference)
{
StringRefNull relative_asset_identifier = asset_reference.relative_asset_identifier;
int64_t blend_ext_pos = rfind_blendfile_extension(asset_reference.relative_asset_identifier);
const bool has_blend_ext = blend_ext_pos != StringRef::not_found;
int64_t blend_path_len = 0;
/* Get the position of the path separator after the blend file extension. */
if (has_blend_ext) {
blend_path_len = relative_asset_identifier.find_first_of(SEP_STR ALTSEP_STR, blend_ext_pos);
/* If there is a blend file in the relative asset path, then there should be group and id name
* after it. */
BLI_assert(blend_path_len != StringRef::not_found);
/* Skip slash. */
blend_path_len += 1;
}
/* Find the first path separator (after the blend file extension if any). This will be the one
* separating the group from the name. */
const int64_t group_name_sep_pos = relative_asset_identifier.find_first_of(SEP_STR ALTSEP_STR,
blend_path_len);
return utils::normalize_path(relative_asset_identifier,
(group_name_sep_pos == StringRef::not_found) ?
StringRef::not_found :
group_name_sep_pos + 1);
}
std::string AssetLibraryService::resolve_asset_weak_reference_to_full_path(
const AssetWeakReference &asset_reference)
{
/* TODO currently only works for asset libraries on disk (custom or essentials asset libraries).
* Once there is a proper registry of asset libraries, this could contain an asset library
* locator and/or identifier, so a full path (not necessarily file path) can be built for all
* asset libraries. */
if (asset_reference.relative_asset_identifier[0] == '\0') {
return "";
}
std::string library_dirpath = resolve_asset_weak_reference_to_library_path(asset_reference);
if (library_dirpath.empty()) {
return "";
}
std::string normalized_full_path = utils::normalize_path(library_dirpath + SEP_STR) +
normalize_asset_weak_reference_relative_asset_identifier(
asset_reference);
return normalized_full_path;
}
std::optional<AssetLibraryService::ExplodedPath> AssetLibraryService::
resolve_asset_weak_reference_to_exploded_path(const AssetWeakReference &asset_reference)
{
if (asset_reference.relative_asset_identifier[0] == '\0') {
return std::nullopt;
}
switch (eAssetLibraryType(asset_reference.asset_library_type)) {
case ASSET_LIBRARY_LOCAL: {
std::string path_in_file = this->normalize_asset_weak_reference_relative_asset_identifier(
asset_reference);
const int64_t group_len = int64_t(path_in_file.find(SEP));
ExplodedPath exploded;
exploded.full_path = std::make_unique<std::string>(path_in_file);
exploded.group_component = StringRef(*exploded.full_path).substr(0, group_len);
exploded.name_component = StringRef(*exploded.full_path).substr(group_len + 1);
return exploded;
}
case ASSET_LIBRARY_CUSTOM:
case ASSET_LIBRARY_ESSENTIALS:
case ASSET_LIBRARY_ONLINE_ESSENTIALS: {
std::string full_path = this->resolve_asset_weak_reference_to_full_path(asset_reference);
/* #full_path uses native slashes, so others don't need to be considered in the following. */
if (full_path.empty()) {
return std::nullopt;
}
int64_t blendfile_extension_pos = this->rfind_blendfile_extension(full_path);
BLI_assert(blendfile_extension_pos != StringRef::not_found);
size_t group_pos = full_path.find(SEP, blendfile_extension_pos);
BLI_assert(group_pos != std::string::npos);
size_t name_pos = full_path.find(SEP, group_pos + 1);
BLI_assert(group_pos != std::string::npos);
const int64_t dir_len = int64_t(group_pos);
const int64_t group_len = int64_t(name_pos - group_pos - 1);
ExplodedPath exploded;
exploded.full_path = std::make_unique<std::string>(full_path);
StringRef full_path_ref = *exploded.full_path;
exploded.dir_component = full_path_ref.substr(0, dir_len);
exploded.group_component = full_path_ref.substr(dir_len + 1, group_len);
exploded.name_component = full_path_ref.substr(dir_len + 1 + group_len + 1);
return exploded;
}
case ASSET_LIBRARY_ALL:
return std::nullopt;
}
return std::nullopt;
}
bUserAssetLibrary *AssetLibraryService::find_custom_asset_library_from_library_ref(
const AssetLibraryReference &library_reference)
{
BLI_assert(library_reference.type == ASSET_LIBRARY_CUSTOM);
BLI_assert(library_reference.custom_library_index >= 0);
return BKE_preferences_asset_library_find_index(&U, library_reference.custom_library_index);
}
std::string AssetLibraryService::root_path_from_library_ref(
const AssetLibraryReference &library_reference)
{
if (ELEM(library_reference.type, ASSET_LIBRARY_ALL, ASSET_LIBRARY_LOCAL)) {
return "";
}
if (ELEM(library_reference.type, ASSET_LIBRARY_ESSENTIALS)) {
return essentials_directory_path();
}
if (library_reference.type == ASSET_LIBRARY_ONLINE_ESSENTIALS) {
return online_essentials_cache_directory_path();
}
bUserAssetLibrary *custom_library = find_custom_asset_library_from_library_ref(
library_reference);
if (!custom_library || !custom_library->dirpath[0]) {
return "";
}
return custom_library->dirpath;
}
void AssetLibraryService::allocate_service_instance()
{
instance_ = std::make_unique<AssetLibraryService>();
instance_->app_handler_register();
if (!atexit_handler_registered_) {
/* Ensure the instance gets freed before Blender's memory leak detector runs. */
BKE_blender_atexit_register([](void * /*user_data*/) { AssetLibraryService::destroy(); },
nullptr);
atexit_handler_registered_ = true;
}
}
static void on_blendfile_load(Main * /*bmain*/,
PointerRNA ** /*pointers*/,
const int /*num_pointers*/,
void * /*arg*/)
{
#ifdef WITH_DESTROY_VIA_LOAD_HANDLER
AssetLibraryService::destroy();
#endif
}
void AssetLibraryService::app_handler_register()
{
/* The callback system doesn't own `on_load_callback_store_`. */
on_load_callback_store_.alloc = false;
on_load_callback_store_.func = &on_blendfile_load;
on_load_callback_store_.arg = this;
BKE_callback_add(&on_load_callback_store_, BKE_CB_EVT_LOAD_PRE);
}
void AssetLibraryService::app_handler_unregister()
{
BKE_callback_remove(&on_load_callback_store_, BKE_CB_EVT_LOAD_PRE);
on_load_callback_store_.func = nullptr;
on_load_callback_store_.arg = nullptr;
}
bool AssetLibraryService::has_any_unsaved_catalogs() const
{
bool has_unsaved_changes = false;
foreach_loaded_asset_library(
[&has_unsaved_changes](AssetLibrary &library) {
if (library.catalog_service().has_unsaved_changes()) {
has_unsaved_changes = true;
}
},
true);
return has_unsaved_changes;
}
void AssetLibraryService::foreach_loaded_asset_library(FunctionRef<void(AssetLibrary &)> fn,
const bool include_all_library) const
{
/* Collect the libraries to visit first, then invoke the callback without holding any of the
* library mutexes. The callback may re-enter the asset library service, e.g. the "All" library
* reading triggers a catalog rebuild, which itself calls #foreach_loaded() - so running it while
* holding these mutexes can deadlock.
*
* Holding on to the raw pointers is safe as long as loaded libraries are not freed concurrently.
*/
Vector<AssetLibrary *, 16> libraries;
if (include_all_library && all_library_) {
libraries.append(all_library_.get());
}
if (current_file_library_) {
libraries.append(current_file_library_.get());
}
{
std::scoped_lock lock{on_disk_libraries_mutex_};
/* Do essentials library first. Plenty of general features use the essentials, these features
* should be available as soon as possible. Not only after other, potentially big libraries are
* loaded. */
for (const auto &asset_lib_uptr : on_disk_libraries_.values()) {
if (asset_lib_uptr->library_type() != ASSET_LIBRARY_ESSENTIALS) {
continue;
}
if (asset_lib_uptr->is_enabled()) {
libraries.append(asset_lib_uptr.get());
}
break;
}
}
const bool include_remote_libraries = USER_EXPERIMENTAL_TEST(&U, use_remote_asset_libraries);
if (include_remote_libraries && online_essentials_library_ &&
(U.asset_flag & USER_ASSETS_USE_ONLINE_ESSENTIALS))
{
libraries.append(online_essentials_library_.get());
}
{
std::scoped_lock lock{on_disk_libraries_mutex_};
for (const auto &asset_lib_uptr : on_disk_libraries_.values()) {
/* Already handled above. */
if (asset_lib_uptr->library_type() == ASSET_LIBRARY_ESSENTIALS) {
continue;
}
if (asset_lib_uptr->is_enabled()) {
libraries.append(asset_lib_uptr.get());
}
}
}
if (include_remote_libraries) {
std::scoped_lock lock{remote_libraries_mutex_};
for (const auto &asset_lib_uptr : remote_libraries_.values()) {
if (asset_lib_uptr->is_enabled()) {
libraries.append(asset_lib_uptr.get());
}
}
}
for (AssetLibrary *library : libraries) {
fn(*library);
}
}
} // namespace asset_system
} // namespace blender

View File

@@ -0,0 +1,218 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup asset_system
*/
#pragma once
#include <memory>
#include <mutex>
#include <optional>
#include <utility>
#include "AS_asset_library.hh"
#include "BLI_function_ref.hh"
#include "BLI_map.hh"
#include "essentials_library.hh"
namespace blender {
struct AssetLibraryReference;
struct bUserAssetLibrary;
namespace asset_system {
class AllAssetLibrary;
class OnDiskAssetLibrary;
class PreferencesRemoteAssetLibrary;
class RuntimeAssetLibrary;
/**
* Global singleton-ish that provides access to individual #AssetLibrary instances.
*
* Whenever a blend file is loaded, the existing instance of AssetLibraryService is destructed, and
* a new one is created -- hence the "singleton-ish". This ensures only information about relevant
* asset libraries is loaded.
*
* \note How Asset libraries are identified may change in the future.
* For now they are assumed to be:
* - on disk (identified by the absolute directory), or
* - the "current file" library (which is in memory but could have catalogs
* loaded from a file on disk).
*/
class AssetLibraryService {
static std::unique_ptr<AssetLibraryService> instance_;
/**
* Identify libraries with the library type, and the absolute path of the library's root path
* (normalize with #normalize_directory_path()!). The type is relevant since the current file
* library may point to the same path as a custom library.
*/
using OnDiskLibraryIdentifier = std::pair<eAssetLibraryType, std::string>;
/** Mapping of a (type, root path) pair to the AssetLibrary instance.
* Always protect access with #on_disk_libraries_mutex_ below. */
Map<OnDiskLibraryIdentifier, std::unique_ptr<OnDiskAssetLibrary>> on_disk_libraries_;
mutable std::recursive_mutex on_disk_libraries_mutex_;
using URLLibraryIdentifier = std::string;
/** Always protect access with #remote_libraries_mutex_ below. */
Map<URLLibraryIdentifier, std::unique_ptr<PreferencesRemoteAssetLibrary>> remote_libraries_;
mutable std::recursive_mutex remote_libraries_mutex_;
/**
* Library without a known path, i.e. the "Current File" library if the file isn't saved yet. If
* the file was saved, a valid path for the library can be determined and #on_disk_libraries_
* above should be used.
*/
std::unique_ptr<RuntimeAssetLibrary> current_file_library_;
/** The "all" asset library, merging all other libraries into one. */
std::unique_ptr<AllAssetLibrary> all_library_;
std::unique_ptr<OnlineEssentialsLibrary> online_essentials_library_;
/** Handlers for managing the life cycle of the AssetLibraryService instance. */
bCallbackFuncStore on_load_callback_store_;
static bool atexit_handler_registered_;
public:
AssetLibraryService() = default;
~AssetLibraryService() = default;
/** Return the AssetLibraryService singleton, allocating it if necessary. */
static AssetLibraryService *get();
/** Destroy the AssetLibraryService singleton. It will be reallocated by #get() if necessary. */
static void destroy();
static std::string root_path_from_library_ref(const AssetLibraryReference &library_reference);
static bUserAssetLibrary *find_custom_asset_library_from_library_ref(
const AssetLibraryReference &library_reference);
static bUserAssetLibrary *find_custom_preferences_asset_library_from_asset_weak_ref(
const AssetWeakReference &asset_reference);
/**
* Turn the runtime current file library into an on-disk current file library, preserving
* catalog data like undo/redo history, deleted catalog info, catalog saving state, etc.
* Note that this creates a new on-disk asset library and destroys the runtime one.
*
* Call when the `.blend` file is saved to disk.
*
* \return the new on-disk current file asset library (null in case of failure to find a path to
* store the library in, based on the #Main.filepath from \a main).
*/
static AssetLibrary *move_runtime_current_file_into_on_disk_library(const Main &bmain);
AssetLibrary *get_asset_library(const Main *bmain,
const AssetLibraryReference &library_reference);
/**
* Get an asset library of type #ASSET_LIBRARY_CUSTOM from a directory path. Use
* #get_asset_library_on_disk_custom_preferences() for asset libraries registered in the
* Preferences.
*/
AssetLibrary *get_asset_library_on_disk_custom(StringRef name, StringRefNull root_path);
/**
* Get an asset library of type #ASSET_LIBRARY_CUSTOM from an asset library definition in the
* Preferences.
*/
AssetLibrary *get_asset_library_on_disk_custom_preferences(bUserAssetLibrary *custom_library);
/** Get a builtin (not user defined) asset library. I.e. a library that is **not** of type
* #ASSET_LIBRARY_CUSTOM. */
AssetLibrary *get_asset_library_on_disk_builtin(eAssetLibraryType type, StringRefNull root_path);
/** Get the "Current File" asset library. */
AssetLibrary *get_asset_library_current_file();
/** Get the "All" asset library, which loads all others and merges them into one. */
AssetLibrary *get_asset_library_all(const Main *bmain);
/**
* Tag the "All" asset library as needing to reload catalogs. This should be called when catalog
* data of other asset libraries changes. Note that changes to the catalog definition file on
* disk don't ever affect this "dirty" flag. It only reflects changes from this Blender session.
*/
void tag_all_library_catalogs_dirty();
void reload_all_library_catalogs_if_dirty();
/**
* Return the start position of the last blend-file extension in given path,
* or #std::string::npos if not found. Works with both kind of path separators.
*/
int64_t rfind_blendfile_extension(StringRef path);
/**
* Return a normalized version of #AssetWeakReference.relative_asset_identifier.
* Special care is required here because slashes or backslashes should not be converted in the ID
* name itself.
*/
std::string normalize_asset_weak_reference_relative_asset_identifier(
const AssetWeakReference &asset_reference);
/** Get a valid library path from the weak reference. Empty if e.g. the reference is to a local
* asset. */
std::string resolve_asset_weak_reference_to_library_path(
const AssetWeakReference &asset_reference);
/**
* Attempt to build a full path to an asset based on the currently available (not necessary
* loaded) asset libraries. The path is not guaranteed to exist. The returned path will be
* normalized and using native slashes.
*
* \note Only works for asset libraries on disk (others can't be resolved).
*/
std::string resolve_asset_weak_reference_to_full_path(const AssetWeakReference &asset_reference);
/** Struct to hold results from path explosion functions
* (#resolve_asset_weak_reference_to_exploded_path()). */
struct ExplodedPath {
/** The string buffer containing the fully resolved path, if resolving was successful. Pointer
* so that the contained string address doesn't change when moving this object. */
std::unique_ptr<std::string> full_path;
/** Reference into the part of #full_path that is the library directory path. That is, it ends
* with the library .blend file ("directory" is misleading). */
StringRef dir_component = "";
/** Reference into the part of #full_path that is the ID group name ("Object", "Material",
* "Brush", ...). */
StringRef group_component = "";
/** Reference into the part of #full_path that is the ID name. */
StringRef name_component = "";
};
/** Similar to #BKE_blendfile_library_path_explode, returns the full path as
* #resolve_asset_weak_reference_to_library_path, with StringRefs to the `dir` (i.e. blendfile
* path), `group` (i.e. ID type) and `name` (i.e. ID name) parts. */
std::optional<ExplodedPath> resolve_asset_weak_reference_to_exploded_path(
const AssetWeakReference &asset_reference);
/** Returns whether there are any known asset libraries with unsaved catalog edits. */
bool has_any_unsaved_catalogs() const;
/** See AssetLibrary::foreach_loaded(). */
void foreach_loaded_asset_library(FunctionRef<void(AssetLibrary &)> fn,
bool include_all_library) const;
protected:
/** Allocate a new instance of the service and assign it to `instance_`. */
static void allocate_service_instance();
OnDiskAssetLibrary *lookup_on_disk_library(eAssetLibraryType type, StringRefNull root_path);
AssetLibrary *find_loaded_on_disk_asset_library_from_name(StringRef name) const;
AssetLibrary *get_online_essentials_asset_library();
AssetLibrary *get_preferences_remote_asset_library(const bUserAssetLibrary &custom_library);
/**
* Get the given asset library. Opens it (i.e. creates a new AssetLibrary instance) if necessary.
*
* \param root_path: The top level directory.
* \param preferences_library: The definition of the library from the Preferences. Set this to
* null if the library is not registered in the Preferences (but non-null if it is!).
*/
AssetLibrary *get_asset_library_on_disk(eAssetLibraryType library_type,
StringRef name,
StringRefNull root_path,
bool load_catalogs = true,
bUserAssetLibrary *preferences_library = nullptr);
/**
* Ensure the AssetLibraryService instance is destroyed before a new blend file is loaded.
* This makes memory management simple, and ensures a fresh start for every blend file. */
void app_handler_register();
void app_handler_unregister();
};
} // namespace asset_system
} // namespace blender

View File

@@ -0,0 +1,350 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup asset_system
*/
#include <stdexcept>
#include "BLI_path_utils.hh"
#include "BKE_blendfile.hh"
#include "BKE_icons.hh"
#include "BKE_idtype.hh"
#include "BKE_lib_id.hh"
#include "BKE_preview_image.hh"
#include "DNA_ID.h"
#include "DNA_asset_types.h"
#include "IMB_thumbs.hh"
#include "AS_asset_library.hh"
#include "AS_asset_representation.hh"
#include "AS_remote_library.hh"
namespace blender::asset_system {
AssetRepresentation::AssetRepresentation(StringRef relative_asset_path,
StringRef name,
const int id_type,
std::unique_ptr<AssetMetaData> metadata,
AssetLibrary &owner_asset_library)
: owner_asset_library_(owner_asset_library),
relative_identifier_(relative_asset_path),
asset_(AssetRepresentation::ExternalAsset{name, id_type, std::move(metadata)})
{
}
AssetRepresentation::AssetRepresentation(StringRef relative_asset_path,
StringRef name,
const int id_type,
std::unique_ptr<AssetMetaData> metadata,
AssetLibrary &owner_asset_library,
OnlineAssetInfo online_info)
: owner_asset_library_(owner_asset_library),
relative_identifier_(relative_asset_path),
asset_(AssetRepresentation::ExternalAsset{
name,
id_type,
std::move(metadata),
nullptr,
RemoteAssetFileStatus::UNSET,
std::make_unique<OnlineAssetInfo>(std::move(online_info))})
{
}
AssetRepresentation::AssetRepresentation(ID &id, AssetLibrary &owner_asset_library)
: owner_asset_library_(owner_asset_library), asset_(&id)
{
if (!id.asset_data) {
throw std::invalid_argument("Passed ID is not an asset");
}
}
AssetRepresentation::~AssetRepresentation()
{
if (const ExternalAsset *extern_asset = std::get_if<ExternalAsset>(&asset_);
extern_asset && extern_asset->preview_)
{
BKE_previewimg_cached_release(this->full_path().c_str());
}
}
AssetWeakReference AssetRepresentation::make_weak_reference() const
{
return AssetWeakReference::make_reference(owner_asset_library_, library_relative_identifier());
}
void AssetRepresentation::ensure_previewable(const bContext &C, ReportList *reports)
{
if (ID *id = this->local_id()) {
PreviewImage *preview = BKE_previewimg_id_get(id);
BKE_icon_preview_ensure(id, preview);
return;
}
ExternalAsset &extern_asset = std::get<ExternalAsset>(asset_);
if (extern_asset.preview_ && extern_asset.preview_->runtime->icon_id) {
return;
}
/* The asset may be in multiple libraries, so multiple #AssetRepresentation's may refer to the
* same preview. Use user counting so the preview is only released with the last representation.
*/
const bool count_preview_users = true;
/* Only use the remote thumbnail when there is no asset file on disk. Otherwise use the on-disk
* file. */
if (this->is_online_only()) {
if (!extern_asset.online_info_->preview_url) {
return;
}
const std::string preview_path = remote_library_asset_preview_path(*this);
/* Doesn't do the actual reading, just allocates and attaches the derived load info. */
extern_asset.preview_ = BKE_previewimg_online_thumbnail_read(
this->full_path().c_str(), preview_path.c_str(), false, count_preview_users);
remote_library_request_preview_download(C, *this, preview_path, reports);
}
else {
/* Use the full path as preview name, it's the only unique identifier we have. */
const std::string full_path = this->full_path();
/* Doesn't do the actual reading, just allocates and attaches the derived load info. */
extern_asset.preview_ = BKE_previewimg_cached_thumbnail_read(
full_path.c_str(), full_path.c_str(), THB_SOURCE_BLEND, false, count_preview_users);
}
BKE_icon_preview_ensure(nullptr, extern_asset.preview_);
}
PreviewImage *AssetRepresentation::get_preview() const
{
if (const ID *id = this->local_id()) {
return BKE_previewimg_id_get(id);
}
return std::get<ExternalAsset>(asset_).preview_;
}
StringRefNull AssetRepresentation::get_name() const
{
if (const ID *id = this->local_id()) {
return id->name + 2;
}
return std::get<ExternalAsset>(asset_).name;
}
ID_Type AssetRepresentation::get_id_type() const
{
if (const ID *id = this->local_id()) {
return GS(id->name);
}
return ID_Type(std::get<ExternalAsset>(asset_).id_type);
}
AssetMetaData &AssetRepresentation::get_metadata() const
{
if (const ID *id = this->local_id()) {
return *id->asset_data;
}
return *std::get<ExternalAsset>(asset_).metadata_;
}
StringRefNull AssetRepresentation::library_relative_identifier() const
{
if (const ID *id = this->local_id()) {
StringRef idname = BKE_id_name(*id);
/* Lazy-create/-update with the latest ID name. */
if (!StringRef{relative_identifier_}.endswith(idname)) {
relative_identifier_ = StringRef{BKE_idtype_idcode_to_name(GS(id->name))} + SEP_STR + idname;
}
}
return relative_identifier_;
}
std::string AssetRepresentation::full_path() const
{
char filepath[FILE_MAX];
BLI_path_join(filepath,
sizeof(filepath),
owner_asset_library_.root_path().c_str(),
library_relative_identifier().c_str());
return filepath;
}
std::string AssetRepresentation::full_library_path() const
{
std::string asset_path = full_path();
char blend_path[/*FILE_MAX_LIBEXTRA*/ 1282];
if (!BKE_blendfile_library_path_explode(asset_path.c_str(), blend_path, nullptr, nullptr)) {
return {};
}
return blend_path;
}
Span<OnlineAssetFile> AssetRepresentation::online_asset_files() const
{
const ExternalAsset *extern_asset = std::get_if<ExternalAsset>(&asset_);
if (!extern_asset || !extern_asset->online_info_) {
return {};
}
return extern_asset->online_info_->files;
}
std::optional<int64_t> AssetRepresentation::online_asset_files_combined_size_in_bytes() const
{
const ExternalAsset *extern_asset = std::get_if<ExternalAsset>(&asset_);
if (!extern_asset || !extern_asset->online_info_) {
return {};
}
int64_t size = 0;
for (const OnlineAssetFile &file : online_asset_files()) {
size += file.size_in_bytes;
}
return size;
}
std::optional<StringRefNull> AssetRepresentation::online_asset_preview_url() const
{
if (!this->is_online_only()) {
return {};
}
std::optional<URLWithHash> &url_with_hash =
std::get<ExternalAsset>(asset_).online_info_->preview_url;
if (!url_with_hash) {
return {};
}
return url_with_hash->url;
}
std::optional<StringRefNull> AssetRepresentation::online_asset_preview_hash() const
{
if (!this->is_online_only()) {
return {};
}
std::optional<URLWithHash> &url_with_hash =
std::get<ExternalAsset>(asset_).online_info_->preview_url;
if (!url_with_hash) {
return {};
}
return url_with_hash->hash;
}
void AssetRepresentation::online_asset_mark_downloaded()
{
ExternalAsset *extern_asset = std::get_if<ExternalAsset>(&asset_);
if (!extern_asset) {
return;
}
/* Since it was just downloaded, let's assume the file matches the listed hash. If not, the
* next refresh will show the correct status.
* TODO: ensure that the file status is actually checked, instead of just making assumptions. */
extern_asset->remote_file_status_ = RemoteAssetFileStatus::MATCH;
}
std::optional<eAssetImportMethod> AssetRepresentation::get_import_method() const
{
const AssetMetaData &metadata = this->get_metadata();
if (metadata.flag & ASSETDATA_USE_OWN_IMPORT_METHOD) {
return metadata.preferred_import_method;
}
return owner_asset_library_.import_method();
}
bool AssetRepresentation::may_override_import_method() const
{
if (!owner_asset_library_.import_method()) {
return true;
}
return owner_asset_library_.may_override_import_method_;
}
bool AssetRepresentation::get_use_relative_path() const
{
return owner_asset_library_.use_relative_paths();
}
ID *AssetRepresentation::local_id() const
{
return this->is_local_id() ? std::get<ID *>(asset_) : nullptr;
}
bool AssetRepresentation::is_local_id() const
{
return std::holds_alternative<ID *>(asset_);
}
bool AssetRepresentation::is_online_only() const
{
const ExternalAsset *extern_asset = std::get_if<ExternalAsset>(&asset_);
if (!extern_asset || !extern_asset->online_info_) {
return false;
}
/* An asset is considered 'online' if there is no file on disk for it.
*
* About also allowing UNSET: This function is (indirectly) called from all kinds of
* places, like `get_node_tools_type_data()` in `node_group_operators.cc` to figure out which
* node tools are available. Since that happens on startup, the actual on-disk file status may
* not have been checked yet. Until that time, just assume that having `online_info_` means "it
* is online". */
return ELEM(extern_asset->remote_file_status_,
RemoteAssetFileStatus::NOT_ON_DISK,
RemoteAssetFileStatus::UNSET);
}
bool AssetRepresentation::is_potentially_editable_asset_blend() const
{
if (this->owner_asset_library().is_read_only()) {
return false;
}
std::string lib_path = this->full_library_path();
return StringRef(lib_path).endswith(BLENDER_ASSET_FILE_SUFFIX);
}
RemoteAssetFileStatus AssetRepresentation::remote_file_status() const
{
const ExternalAsset *extern_asset = std::get_if<ExternalAsset>(&asset_);
if (!extern_asset) {
return RemoteAssetFileStatus::UNSET;
}
return extern_asset->remote_file_status_;
}
void AssetRepresentation::online_info_set(OnlineAssetInfo info)
{
ExternalAsset *extern_asset = std::get_if<ExternalAsset>(&asset_);
if (!extern_asset) {
return;
}
extern_asset->online_info_ = std::make_unique<OnlineAssetInfo>(std::move(info));
}
void AssetRepresentation::remote_file_status_set(const RemoteAssetFileStatus status)
{
ExternalAsset *extern_asset = std::get_if<ExternalAsset>(&asset_);
if (!extern_asset) {
return;
}
extern_asset->remote_file_status_ = status;
}
bool AssetRepresentation::needs_download() const
{
return this->is_online_only() || this->remote_file_status() == RemoteAssetFileStatus::NO_MATCH;
}
AssetLibrary &AssetRepresentation::owner_asset_library() const
{
return owner_asset_library_;
}
} // namespace blender::asset_system

View File

@@ -0,0 +1,183 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup asset_system
*/
#include <optional>
#include "AS_disk_file_hash_service.hh"
#include "BKE_idprop.hh"
#ifdef WITH_PYTHON
# include "BPY_extern_run.hh"
#endif
#include "CLG_log.h"
static CLG_LogRef LOG = {"assets.disk_file_hash_service"};
namespace blender::asset_system {
std::unique_ptr<DiskFileHashService> disk_file_hash_service_get(const StringRef storage_path)
{
return std::make_unique<DiskFileHashService>(storage_path);
}
DiskFileHashService::DiskFileHashService(const StringRef storage_path)
: storage_path_(storage_path)
{
}
DiskFileHashService::~DiskFileHashService()
{
release_python();
}
void DiskFileHashService::release_python()
{
#ifdef WITH_PYTHON
constexpr const char *SCRIPT = R"(
import _bpy_internal.disk_file_hash_service as dfhs
from pathlib import Path
dfhs.release_service(Path(storage_path))
)";
std::unique_ptr locals = bke::idprop::create_group("locals");
IDP_AddToGroup(locals.get(), IDP_NewString(this->storage_path_, "storage_path"));
BPY_run_string_exec_with_locals(nullptr, SCRIPT, *locals);
#endif
}
std::string DiskFileHashService::get_hash(const StringRef filepath, const StringRef hash_algorithm)
{
#ifdef WITH_PYTHON
/* NOTE: this is a somewhat inefficient implementation for frequently-repeated calls, as each
* call repeats the calls to `dfhs.get_service(Path(...))`. However, this does mean that the C++
* wrapper does not have to retain any references to Python objects itself, avoiding reference
* counting bugs. If the performance starts to matter, do the lookup of the service itself once,
* and cache the result. */
constexpr const char *SCRIPT = R"(
import _bpy_internal.disk_file_hash_service as dfhs
from pathlib import Path
service = dfhs.get_service(Path(storage_path))
_result = service.get_hash(Path(filepath), hash_algorithm)
)";
/* Local variables for the script. */
std::unique_ptr locals = bke::idprop::create_group("locals");
IDP_AddToGroup(locals.get(), IDP_NewString(this->storage_path_, "storage_path"));
IDP_AddToGroup(locals.get(), IDP_NewString(filepath, "filepath"));
IDP_AddToGroup(locals.get(), IDP_NewString(hash_algorithm, "hash_algorithm"));
/* Run the script. */
std::optional<IDProperty *> idprop_optptr = BPY_run_string_exec_with_locals_return_idprop(
nullptr, SCRIPT, *locals, "_result");
if (!idprop_optptr.has_value()) {
const std::string filepath_str = filepath;
CLOG_ERROR(&LOG, "Failed to run hash script for file [%s].", filepath_str.c_str());
return "";
}
IDProperty *hash_idprop = *idprop_optptr;
/* Check the returned value. */
if (hash_idprop == nullptr || hash_idprop->type != IDP_STRING) {
IDP_FreeProperty(hash_idprop);
const std::string filepath_str = filepath;
CLOG_ERROR(&LOG,
"Hash for file [%s] was not returned as string. Please report this as a bug.",
filepath_str.c_str());
return "";
}
const std::string hash_value(IDP_string_get(hash_idprop));
IDP_FreeProperty(hash_idprop);
return hash_value;
#else
UNUSED_VARS(filepath, hash_algorithm);
const std::string filepath_str = filepath;
CLOG_ERROR(&LOG,
"Blender was built without Python support, cannot compute hash for file [%s]",
filepath_str.c_str());
return "";
#endif
}
bool DiskFileHashService::file_matches(const StringRef filepath,
const StringRef hash_algorithm,
const StringRef hexhash,
const int64_t size_in_bytes)
{
#ifdef WITH_PYTHON
/* NOTE: this is a somewhat inefficient implementation for frequently-repeated calls, as each
* call repeats the calls to `dfhs.get_service(Path(...))`. However, this does mean that the C++
* wrapper does not have to retain any references to Python objects itself, avoiding reference
* counting bugs. If the performance starts to matter, do the lookup of the service itself once,
* and cache the result. */
constexpr const char *SCRIPT = R"(
import _bpy_internal.disk_file_hash_service as dfhs
from pathlib import Path
# The '& 0xFFFFFFFF' makes Python interpret the values as unsigned ints.
size_in_bytes = ((size_in_bytes_high & 0xFFFFFFFF) << 32) | (size_in_bytes_low & 0xFFFFFFFF)
service = dfhs.get_service(Path(storage_path))
_result = service.file_matches(Path(filepath), hash_algorithm, hexhash, size_in_bytes);
)";
/* Since IDProperties don't support 64-bit integers, split it up into two 32-bit integers, and do
* bit shifting in Python to get the value back. */
BLI_assert(size_in_bytes >= 0);
const int size_in_bytes_high = int((size_in_bytes >> 32) & 0xFFFFFFFF);
const int size_in_bytes_low = int(size_in_bytes & 0xFFFFFFFF);
std::unique_ptr locals = bke::idprop::create_group("locals");
IDP_AddToGroup(locals.get(), IDP_NewString(this->storage_path_, "storage_path"));
IDP_AddToGroup(locals.get(), IDP_NewString(filepath, "filepath"));
IDP_AddToGroup(locals.get(), IDP_NewString(hash_algorithm, "hash_algorithm"));
IDP_AddToGroup(locals.get(), IDP_NewString(hexhash, "hexhash"));
IDP_AddToGroup(locals.get(), IDP_NewInt(size_in_bytes_high, "size_in_bytes_high"));
IDP_AddToGroup(locals.get(), IDP_NewInt(size_in_bytes_low, "size_in_bytes_low"));
/* Run the script. */
std::optional<IDProperty *> idprop_optptr = BPY_run_string_exec_with_locals_return_idprop(
nullptr, SCRIPT, *locals, "_result");
if (!idprop_optptr.has_value()) {
const std::string filepath_str = filepath;
CLOG_ERROR(&LOG, "Failed to run hash match script for file [%s].", filepath_str.c_str());
return false;
}
IDProperty *is_match_idprop = *idprop_optptr;
/* Check the returned value. */
if (is_match_idprop == nullptr || is_match_idprop->type != IDP_BOOLEAN) {
IDP_FreeProperty(is_match_idprop);
const std::string filepath_str = filepath;
CLOG_ERROR(
&LOG,
"Hash match check for file [%s] did not return a boolean. Please report this as a bug.",
filepath_str.c_str());
return false;
}
const bool is_match(IDP_bool_get(is_match_idprop));
IDP_FreeProperty(is_match_idprop);
return is_match;
#else
UNUSED_VARS(filepath, hash_algorithm, hexhash, size_in_bytes);
const std::string filepath_str = filepath;
CLOG_ERROR(&LOG,
"Blender was built without Python support, cannot check hash for file [%s]",
filepath_str.c_str());
return false;
#endif
}
} // namespace blender::asset_system

View File

@@ -0,0 +1,132 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup asset_system
*/
#include <memory>
/* For getting the experimental flag for remote library support. */
#include "DNA_userdef_types.h"
#include "AS_remote_library.hh"
#include "all_library.hh"
#include "CLG_log.h"
namespace blender {
static CLG_LogRef LOG = {"asset.library"};
namespace asset_system {
AllAssetLibrary::AllAssetLibrary()
: AssetLibrary(ASSET_LIBRARY_ALL,
/*is_read_only=*/true)
{
}
void AllAssetLibrary::force_remote_listing_download() const
{
/* This includes the online essentials as a separate library, if loaded. */
AssetLibrary::foreach_loaded(
[&](AssetLibrary &nested) {
const std::optional<StringRefNull> url = nested.remote_url();
if (url.has_value()) {
remote_library_request_download(RemoteLibraryDefinitionRef{*url, nested.root_path()});
}
},
/*include_all_library=*/false);
}
std::optional<AssetLibraryReference> AllAssetLibrary::library_reference() const
{
return all_library_reference();
}
std::optional<eAssetImportMethod> AllAssetLibrary::import_method() const
{
return {};
}
void AllAssetLibrary::rebuild_catalogs_from_nested(const bool reload_nested_catalogs)
{
/* Only one thread should rebuild at a time. If another thread is already rebuilding, wait for it
* to finish and then skip rebuilding. The result would effectively be the same, so re-running
* would just be wasted work. Waiting (rather than returning early) ensures callers don't see
* partially rebuilt catalogs. */
std::unique_lock rebuild_lock{rebuild_mutex_, std::try_to_lock};
if (!rebuild_lock.owns_lock()) {
/* Another thread holds the lock and is rebuilding. Block until it is done, then return. */
rebuild_lock.lock();
return;
}
/* Start with empty catalog storage. Don't do this directly in #this.catalog_service to avoid
* race conditions. Rather build into a new service and replace the current one when done. */
std::unique_ptr<AssetCatalogService> new_catalog_service = std::make_unique<AssetCatalogService>(
AssetCatalogService::read_only_tag());
const bool skip_remote_libraries = !USER_EXPERIMENTAL_TEST(&U, use_remote_asset_libraries);
AssetLibrary::foreach_loaded(
[&](AssetLibrary &nested) {
const bool is_online_lib = nested.remote_url().has_value();
if (is_online_lib && skip_remote_libraries) {
return;
}
if (reload_nested_catalogs) {
nested.catalog_service().reload_catalogs();
}
new_catalog_service->add_from_existing(
nested.catalog_service(),
/*on_duplicate_items=*/[](const AssetCatalog &existing,
const AssetCatalog &to_be_ignored) {
if (existing.path == to_be_ignored.path) {
CLOG_DEBUG(&LOG,
"multiple definitions of catalog %s (path: %s), ignoring duplicate",
existing.catalog_id.str().c_str(),
existing.path.c_str());
}
else {
/* This is bound to happen at some point, for example with the Online Essentials
* catalogs diverging from this Blender version's bundled Essentials catalogs. */
CLOG_INFO(&LOG,
"multiple definitions of catalog %s with differing paths (%s vs. %s), "
"ignoring second one",
existing.catalog_id.str().c_str(),
existing.path.c_str(),
to_be_ignored.path.c_str());
}
});
},
false);
std::lock_guard lock{catalog_service_mutex_};
catalog_service_ = std::move(new_catalog_service);
catalogs_dirty_ = false;
}
void AllAssetLibrary::tag_catalogs_dirty()
{
catalogs_dirty_ = true;
}
bool AllAssetLibrary::is_catalogs_dirty() const
{
return catalogs_dirty_;
}
void AllAssetLibrary::refresh_catalogs()
{
this->rebuild_catalogs_from_nested(/*reload_nested_catalogs=*/true);
}
} // namespace asset_system
} // namespace blender

View File

@@ -0,0 +1,46 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup asset_system
*/
#pragma once
#include <atomic>
#include <mutex>
#include "AS_asset_library.hh"
namespace blender::asset_system {
class AllAssetLibrary : public AssetLibrary {
std::atomic<bool> catalogs_dirty_ = true;
/** Serializes #rebuild_catalogs_from_nested so only one thread rebuilds at a time. */
std::mutex rebuild_mutex_;
public:
AllAssetLibrary();
void force_remote_listing_download() const override;
std::optional<AssetLibraryReference> library_reference() const override;
std::optional<eAssetImportMethod> import_method() const override;
void refresh_catalogs() override;
/**
* Update the available catalogs and catalog tree from the nested asset libraries. Completely
* recreates the catalog service (invalidating pointers to the previous one).
*
* \param reload_nested_catalogs: Re-read catalog definitions of nested libraries from disk and
* merge them into the in-memory representations.
*/
void rebuild_catalogs_from_nested(bool reload_nested_catalogs);
void tag_catalogs_dirty();
bool is_catalogs_dirty() const;
};
} // namespace blender::asset_system

View File

@@ -0,0 +1,34 @@
/* SPDX-FileCopyrightText: 2026 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup asset_system
*/
#include "BLI_listbase.h"
#include "DNA_userdef_types.h"
#include "common.hh"
namespace blender::asset_system {
UserAssetLibraryWrapper::UserAssetLibraryWrapper(const bUserAssetLibrary &user_asset_library)
: user_asset_library_(&user_asset_library)
{
}
const bUserAssetLibrary *UserAssetLibraryWrapper::user_asset_library() const
{
if (user_asset_library_ == nullptr) {
return nullptr;
}
if (BLI_findindex(&U.asset_libraries, user_asset_library_) == -1) {
user_asset_library_ = nullptr;
return nullptr;
}
return user_asset_library_;
}
} // namespace blender::asset_system

View File

@@ -0,0 +1,41 @@
/* SPDX-FileCopyrightText: 2026 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup asset_system
*/
#pragma once
namespace blender {
struct bUserAssetLibrary;
}
namespace blender::asset_system {
/**
* Wrapper to get the #bUserAssetLibrary from the preferences (if still valid).
*/
class UserAssetLibraryWrapper {
/**
* Pointer to the user's asset library entry in the preferences.
* \warning This may be dangling or null! Only access this using #user_asset_library(), which
* returns `nullptr` if the library is not found (meaning it was removed/freed). It will also
* null the pointer in that case, to avoid holding on to the dangling pointer (that's why it's
* mutable).
*/
mutable const bUserAssetLibrary *user_asset_library_;
public:
explicit UserAssetLibraryWrapper(const bUserAssetLibrary &user_asset_library);
/**
* Returns a pointer to the user's asset library entry in the preferences, or `nullptr` if not
* found (meaning it was removed/freed).
*/
const bUserAssetLibrary *user_asset_library() const;
};
} // namespace blender::asset_system

View File

@@ -0,0 +1,206 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup asset_system
*/
#include "AS_remote_library.hh"
#include "BKE_appdir.hh"
#include "BLI_path_utils.hh"
#include "BLI_string_ref.hh"
#include "CLG_log.h"
#include "DNA_asset_types.h"
#include "DNA_userdef_types.h"
#include "on_disk_library.hh"
#include "remote_library.hh"
#include "utils.hh"
#include "AS_essentials_library.hh"
#include "essentials_library.hh"
namespace blender::asset_system {
static CLG_LogRef LOG = {"asset.library.essentials"};
EssentialsAssetLibrary::EssentialsAssetLibrary()
: OnDiskAssetLibrary(ASSET_LIBRARY_ESSENTIALS,
{},
utils::normalize_directory_path(essentials_directory_path()),
/*is_read_only=*/true)
{
}
void EssentialsAssetLibrary::force_remote_listing_download() const
{
remote_library_request_download(RemoteLibraryDefinitionRef{
online_essentials_url(), online_essentials_cache_directory_path()});
}
std::optional<AssetLibraryReference> EssentialsAssetLibrary::library_reference() const
{
AssetLibraryReference library_ref{};
library_ref.custom_library_index = -1;
library_ref.type = ASSET_LIBRARY_ESSENTIALS;
return library_ref;
}
std::optional<eAssetImportMethod> EssentialsAssetLibrary::import_method() const
{
if (U.experimental.no_data_block_packing) {
return ASSET_IMPORT_APPEND_REUSE;
}
return ASSET_IMPORT_PACK;
}
void EssentialsAssetLibrary::refresh_catalogs()
{
/* Start with empty catalog storage. Don't do this directly in #this.catalog_service to avoid
* race conditions. Rather build into a new service and replace the current one when done. */
std::unique_ptr<AssetCatalogService> new_catalog_service = std::make_unique<AssetCatalogService>(
AssetCatalogService::read_only_tag());
const bool skip_remote_libraries = !USER_EXPERIMENTAL_TEST(&U, use_remote_asset_libraries);
const auto load_catalogs_fn = [&](const AssetLibrary *library) {
const bool is_online_lib = library->remote_url().has_value();
if (is_online_lib && skip_remote_libraries) {
return;
}
library->catalog_service().reload_catalogs();
new_catalog_service->add_from_existing(
library->catalog_service(),
/*on_duplicate_items=*/[](const AssetCatalog &existing,
const AssetCatalog &to_be_ignored) {
if (existing.path == to_be_ignored.path) {
CLOG_DEBUG(&LOG,
"multiple definitions of catalog %s (path: %s), ignoring duplicate",
existing.catalog_id.str().c_str(),
existing.path.c_str());
}
else {
/* This is to be expected at some point in the future. The Online Essentials library
* may change its catalog paths, while whatever version of Blender is running right now
* still has the same old bundled assets. This means the Bundled Essentials and Online
* Essentials diverge. There is no need to bother users with this, as it's bound to
* happen eventually.
*
* Note that this same check happens in the 'All' library as well, and that already
* logs this at INFO level, so there really is no need to be louder than DEBUG here. */
CLOG_DEBUG(&LOG,
"multiple definitions of catalog %s with differing paths (%s vs. %s), "
"ignoring second one",
existing.catalog_id.str().c_str(),
existing.path.c_str(),
to_be_ignored.path.c_str());
}
});
};
load_catalogs_fn(this);
if (U.asset_flag & USER_ASSETS_USE_ONLINE_ESSENTIALS) {
load_catalogs_fn(AS_asset_library_load(nullptr, online_essentials_library_reference()));
}
std::lock_guard lock{catalog_service_mutex_};
catalog_service_ = std::move(new_catalog_service);
}
StringRefNull essentials_directory_path()
{
static std::string path = []() {
const std::optional<std::string> datafiles_path = BKE_appdir_folder_id(
BLENDER_SYSTEM_DATAFILES, "assets");
return datafiles_path.value_or("");
}();
return path;
}
bool skip_experimental_asset_catalog(const UUID & /*catalog_id*/)
{
/* Return true when the catalog_id should be rejected based on experimental features:
*
* const UUID UUID_my_feature_catalog_id("11111111-2222-3333-4444-555555555555");
* if (!U.experimental.use_my_feature && catalog_id == UUID_my_feature_catalog_id) {
* return true;
* }
*/
return false;
}
/* -------------------------------------------------------------------- */
/** \name Online Essentials Library
*
* Internally this is a separate library. To the user, it's part of the normal Essentials library.
* \{ */
StringRefNull online_essentials_cache_directory_path()
{
static std::string path = []() {
return remote_library_cache_directory_path("online-essentials");
}();
return path;
}
StringRefNull online_essentials_url()
{
return OnlineEssentialsLibrary::URL;
}
bool is_online_essentials_url(const StringRef url)
{
if (url.is_empty()) {
return false;
}
if (remote_library_url_ends_with_top_meta_file_name(url)) {
BLI_assert(url.drop_suffix(REMOTE_LIBRARY_TOP_META_FILE_NAME.size()).back() == '/');
return url.drop_suffix(REMOTE_LIBRARY_TOP_META_FILE_NAME.size()) ==
OnlineEssentialsLibrary::URL;
}
return url == OnlineEssentialsLibrary::URL;
}
bool is_online_essentials_dirpath(StringRef dirpath)
{
if (dirpath.is_empty()) {
return false;
}
if (dirpath.endswith(SEP_STR)) {
dirpath = dirpath.drop_known_suffix(SEP_STR);
}
BLI_assert(!online_essentials_cache_directory_path().endswith(SEP_STR));
return dirpath == online_essentials_cache_directory_path();
}
OnlineEssentialsLibrary::OnlineEssentialsLibrary()
: RemoteAssetLibrary(ASSET_LIBRARY_ONLINE_ESSENTIALS,
/*is_read_only=*/true,
/*remote_url=*/URL,
/*name=*/"Online Essentials",
/*root_path=*/online_essentials_cache_directory_path())
{
}
std::optional<AssetLibraryReference> OnlineEssentialsLibrary::library_reference() const
{
AssetLibraryReference library_ref{};
library_ref.type = ASSET_LIBRARY_ONLINE_ESSENTIALS;
library_ref.custom_library_index = -1;
return library_ref;
}
/** \} */
} // namespace blender::asset_system

View File

@@ -0,0 +1,40 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup asset_system
*/
#pragma once
#include "BLI_string_ref.hh"
#include "on_disk_library.hh"
#include "remote_library.hh"
namespace blender::asset_system {
class EssentialsAssetLibrary : public OnDiskAssetLibrary {
public:
EssentialsAssetLibrary();
void force_remote_listing_download() const override;
std::optional<AssetLibraryReference> library_reference() const override;
std::optional<eAssetImportMethod> import_method() const override;
void refresh_catalogs() override;
};
class OnlineEssentialsLibrary : public RemoteAssetLibrary {
public:
OnlineEssentialsLibrary();
/* Trailing slash matters! */
static constexpr StringRefNull URL =
"https://cdn.extensions.blender.org/asset-libraries/essentials/";
std::optional<AssetLibraryReference> library_reference() const override;
};
} // namespace blender::asset_system

View File

@@ -0,0 +1,52 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup asset_system
*/
#include "on_disk_library.hh"
namespace blender::asset_system {
OnDiskAssetLibrary::OnDiskAssetLibrary(eAssetLibraryType library_type,
StringRef name,
StringRef root_path,
const bool is_read_only)
: AssetLibrary(library_type, /*is_read_only=*/is_read_only, name, root_path)
{
this->on_blend_save_handler_register();
}
std::optional<AssetLibraryReference> OnDiskAssetLibrary::library_reference() const
{
if (library_type() == ASSET_LIBRARY_LOCAL) {
AssetLibraryReference library_ref{};
library_ref.custom_library_index = -1;
library_ref.type = ASSET_LIBRARY_LOCAL;
return library_ref;
}
BLI_assert_msg(false,
"Library references are only available for built-in libraries and libraries "
"configured in the Preferences");
return {};
}
std::optional<eAssetImportMethod> OnDiskAssetLibrary::import_method() const
{
return {};
}
void OnDiskAssetLibrary::refresh_catalogs()
{
this->catalog_service().reload_catalogs();
}
bool OnDiskAssetLibrary::is_enabled() const
{
return true;
}
} // namespace blender::asset_system

View File

@@ -0,0 +1,29 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup asset_system
*/
#pragma once
#include "AS_asset_library.hh"
namespace blender::asset_system {
class OnDiskAssetLibrary : public AssetLibrary {
public:
OnDiskAssetLibrary(eAssetLibraryType library_type,
StringRef name,
StringRef root_path,
bool is_read_only);
std::optional<AssetLibraryReference> library_reference() const override;
std::optional<eAssetImportMethod> import_method() const override;
void refresh_catalogs() override;
virtual bool is_enabled() const;
};
} // namespace blender::asset_system

View File

@@ -0,0 +1,78 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup asset_system
*/
#include "BLI_assert.h"
#include "BLI_listbase.h"
#include "DNA_userdef_types.h"
#include "common.hh"
#include "preferences_on_disk_library.hh"
namespace blender::asset_system {
PreferencesOnDiskAssetLibrary::PreferencesOnDiskAssetLibrary(
const bUserAssetLibrary &user_asset_library)
: OnDiskAssetLibrary(ASSET_LIBRARY_CUSTOM,
user_asset_library.name,
user_asset_library.dirpath,
/*is_read_only=*/false),
user_library_(user_asset_library)
{
}
std::optional<AssetLibraryReference> PreferencesOnDiskAssetLibrary::library_reference() const
{
const bUserAssetLibrary *library_definition = user_library_.user_asset_library();
if (!library_definition) {
return {};
}
const int index = BLI_findindex(&U.asset_libraries, library_definition);
if (index == -1) {
/* Should have been caught by the #user_asset_library() call above already. */
BLI_assert_unreachable();
return {};
}
AssetLibraryReference library_ref{};
library_ref.type = ASSET_LIBRARY_CUSTOM;
library_ref.custom_library_index = index;
return library_ref;
}
std::optional<eAssetImportMethod> PreferencesOnDiskAssetLibrary::import_method() const
{
const bUserAssetLibrary *library_definition = user_library_.user_asset_library();
if (!library_definition) {
return {};
}
return eAssetImportMethod(library_definition->import_method);
}
bool PreferencesOnDiskAssetLibrary::use_relative_paths() const
{
const bUserAssetLibrary *library_definition = user_library_.user_asset_library();
if (!library_definition) {
return false;
}
return (library_definition->flag & ASSET_LIBRARY_RELATIVE_PATH) != 0;
}
bool PreferencesOnDiskAssetLibrary::is_enabled() const
{
const bUserAssetLibrary *library_definition = user_library_.user_asset_library();
if (!library_definition) {
return false;
}
return (library_definition->flag & ASSET_LIBRARY_DISABLED) == 0;
}
} // namespace blender::asset_system

View File

@@ -0,0 +1,30 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup asset_system
*/
#pragma once
#include "common.hh"
#include "on_disk_library.hh"
namespace blender::asset_system {
class PreferencesOnDiskAssetLibrary : public OnDiskAssetLibrary {
/** Helper to get the #bUserAssetLibrary from the preferences (if still valid). */
UserAssetLibraryWrapper user_library_;
public:
explicit PreferencesOnDiskAssetLibrary(const bUserAssetLibrary &user_asset_library);
std::optional<AssetLibraryReference> library_reference() const override;
std::optional<eAssetImportMethod> import_method() const override;
bool use_relative_paths() const override;
bool is_enabled() const override;
};
} // namespace blender::asset_system

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,47 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup asset_system
*/
#pragma once
#include "common.hh"
#include "AS_asset_library.hh"
namespace blender::asset_system {
/**
* Abstract class for remote libraries. #PreferencesRemoteAssetLibrary and #OnlineEssentialsLibrary
* derive from this.
*/
class RemoteAssetLibrary : public AssetLibrary {
std::string remote_url_;
public:
RemoteAssetLibrary(eAssetLibraryType library_type,
bool is_read_only,
StringRef remote_url,
StringRef name,
StringRef root_path);
void force_remote_listing_download() const override;
std::optional<eAssetImportMethod> import_method() const override;
std::optional<StringRefNull> remote_url() const override;
void refresh_catalogs() override;
};
class PreferencesRemoteAssetLibrary : public RemoteAssetLibrary {
/** Helper to get the #bUserAssetLibrary from the preferences (if still valid). */
UserAssetLibraryWrapper user_library_;
public:
PreferencesRemoteAssetLibrary(const bUserAssetLibrary &custom_library);
std::optional<AssetLibraryReference> library_reference() const override;
bool is_enabled() const;
};
} // namespace blender::asset_system

View File

@@ -0,0 +1,32 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup asset_system
*/
#include "runtime_library.hh"
namespace blender::asset_system {
RuntimeAssetLibrary::RuntimeAssetLibrary()
: AssetLibrary(ASSET_LIBRARY_LOCAL, /*is_read_only=*/false)
{
this->on_blend_save_handler_register();
}
std::optional<AssetLibraryReference> RuntimeAssetLibrary::library_reference() const
{
AssetLibraryReference library_ref{};
library_ref.type = ASSET_LIBRARY_LOCAL;
library_ref.custom_library_index = -1;
return library_ref;
}
std::optional<eAssetImportMethod> RuntimeAssetLibrary::import_method() const
{
return {};
}
} // namespace blender::asset_system

View File

@@ -0,0 +1,26 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup asset_system
*
* An asset library that is purely stored in-memory. Used for the "Current File" asset library
* while the file has not been saved on disk yet.
*/
#pragma once
#include "AS_asset_library.hh"
namespace blender::asset_system {
class RuntimeAssetLibrary : public AssetLibrary {
public:
RuntimeAssetLibrary();
std::optional<AssetLibraryReference> library_reference() const override;
std::optional<eAssetImportMethod> import_method() const override;
};
} // namespace blender::asset_system

View File

@@ -0,0 +1,55 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup asset_system
*/
/* For PATH_MAX (at least on Windows). */
#include "BLI_fileops.h" // IWYU pragma: keep
#include "BLI_path_utils.hh"
#include "BLI_string.h"
#include "MEM_guardedalloc.h"
#include "utils.hh"
namespace blender::asset_system::utils {
std::string normalize_directory_path(StringRef directory)
{
if (directory.is_empty()) {
return "";
}
char dir_normalized[PATH_MAX];
BLI_strncpy(dir_normalized,
directory.data(),
/* + 1 for null terminator. */
std::min(directory.size() + 1, int64_t(sizeof(dir_normalized))));
BLI_path_slash_native(dir_normalized);
BLI_path_normalize_dir(dir_normalized, sizeof(dir_normalized));
return std::string(dir_normalized);
}
std::string normalize_path(StringRefNull path, int64_t max_len)
{
const int64_t len = (max_len == StringRef::not_found) ? path.size() :
std::min(max_len, path.size());
char *buf = BLI_strdupn(path.c_str(), len);
BLI_path_slash_native(buf);
BLI_path_normalize(buf);
std::string normalized_path = buf;
MEM_delete(buf);
if (len != path.size()) {
normalized_path = normalized_path + path.substr(len);
}
return normalized_path;
}
} // namespace blender::asset_system::utils

View File

@@ -0,0 +1,31 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup asset_system
*/
#pragma once
#include "BLI_string_ref.hh"
namespace blender::asset_system::utils {
/**
* Returns a normalized directory path with a trailing slash, and a maximum length of #PATH_MAX.
* Slashes are converted to native format.
*/
std::string normalize_directory_path(StringRef directory);
/**
* Normalize the given `path` (remove 'parent directory' and double-slashes element etc., and
* convert to native path separators).
*
* If \a max_len is not #StringRef::not_found (default value), only the first part of the given
* string up to the given length is processed, the rest remains unchanged. Needed to avoid
* modifying ID name part of linked library paths.
*/
std::string normalize_path(StringRefNull path, int64_t max_len = StringRef::not_found);
} // namespace blender::asset_system::utils

View File

@@ -0,0 +1,273 @@
/* SPDX-FileCopyrightText: 2020 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "AS_asset_catalog_path.hh"
#include "BLI_set.hh"
#include "BLI_vector.hh"
#include "BKE_gtest_base.hh"
#include <set>
#include <sstream>
#include "testing/testing.h"
namespace blender::asset_system::tests {
class AssetCatalogPathTest : public bke::BlenderGTestBase {};
TEST_F(AssetCatalogPathTest, construction)
{
AssetCatalogPath default_constructed;
/* Use `.str()` to use `std:string`'s comparison operators here, not our own (which are tested
* later). */
EXPECT_EQ(default_constructed.str(), "");
/* C++ considers this construction special, it doesn't call the default constructor but does
* recursive, member-wise value initialization. See https://stackoverflow.com/a/4982720. */
AssetCatalogPath value_initialized = AssetCatalogPath();
EXPECT_EQ(value_initialized.str(), "");
AssetCatalogPath from_char_literal("the/path");
const std::string str_const = "the/path";
AssetCatalogPath from_string_constant(str_const);
std::string str_variable = "the/path";
AssetCatalogPath from_string_variable(str_variable);
std::string long_string = "this is a long/string/with/a/path in the middle";
StringRef long_string_ref(long_string);
StringRef middle_bit = long_string_ref.substr(10, 23);
AssetCatalogPath from_string_ref(middle_bit);
EXPECT_EQ(from_string_ref, "long/string/with/a/path");
}
TEST_F(AssetCatalogPathTest, length)
{
const AssetCatalogPath one("1");
EXPECT_EQ(1, one.length());
const AssetCatalogPath empty("");
EXPECT_EQ(0, empty.length());
const AssetCatalogPath utf8("some/родитель");
EXPECT_EQ(21, utf8.length()) << "13 characters should be 21 bytes.";
}
TEST_F(AssetCatalogPathTest, name)
{
EXPECT_EQ(StringRefNull(""), AssetCatalogPath("").name());
EXPECT_EQ(StringRefNull("word"), AssetCatalogPath("word").name());
EXPECT_EQ(StringRefNull("Пермь"), AssetCatalogPath("дорога/в/Пермь").name());
EXPECT_EQ(StringRefNull("windows\\paths"),
AssetCatalogPath("these/are/not/windows\\paths").name());
}
TEST_F(AssetCatalogPathTest, comparison_operators)
{
const AssetCatalogPath empty("");
const AssetCatalogPath the_path("the/path");
const AssetCatalogPath the_path_child("the/path/child");
const AssetCatalogPath unrelated_path("unrelated/path");
const AssetCatalogPath other_instance_same_path("the/path");
EXPECT_LT(empty, the_path);
EXPECT_LT(the_path, the_path_child);
EXPECT_LT(the_path, unrelated_path);
EXPECT_EQ(empty, empty) << "Identical empty instances should compare equal.";
EXPECT_EQ(empty, "") << "Comparison to empty string should be possible.";
EXPECT_EQ(the_path, the_path) << "Identical non-empty instances should compare equal.";
EXPECT_EQ(the_path, "the/path") << "Comparison to string should be possible.";
EXPECT_EQ(the_path, other_instance_same_path)
<< "Different instances with equal path should compare equal.";
EXPECT_NE(the_path, the_path_child);
EXPECT_NE(the_path, unrelated_path);
EXPECT_NE(the_path, empty);
EXPECT_FALSE(empty);
EXPECT_TRUE(the_path);
}
TEST_F(AssetCatalogPathTest, move_semantics)
{
AssetCatalogPath source_path("source/path");
EXPECT_TRUE(source_path);
AssetCatalogPath dest_path = std::move(source_path);
EXPECT_FALSE(source_path); /* NOLINT: bugprone-use-after-move */
EXPECT_TRUE(dest_path);
}
TEST_F(AssetCatalogPathTest, concatenation)
{
AssetCatalogPath some_parent("some/родитель");
AssetCatalogPath child = some_parent / "ребенок";
EXPECT_EQ(some_parent, "some/родитель")
<< "Appending a child path should not modify the parent.";
EXPECT_EQ(child, "some/родитель/ребенок");
AssetCatalogPath appended_compound_path = some_parent / "ребенок/внук";
EXPECT_EQ(appended_compound_path, "some/родитель/ребенок/внук");
AssetCatalogPath empty("");
AssetCatalogPath child_of_the_void = empty / "child";
EXPECT_EQ(child_of_the_void, "child")
<< "Appending to an empty path should not create an initial slash.";
AssetCatalogPath parent_of_the_void = some_parent / empty;
EXPECT_EQ(parent_of_the_void, "some/родитель")
<< "Prepending to an empty path should not create a trailing slash.";
std::string subpath = "child";
AssetCatalogPath concatenated_with_string = some_parent / subpath;
EXPECT_EQ(concatenated_with_string, "some/родитель/child");
}
TEST_F(AssetCatalogPathTest, hashable)
{
AssetCatalogPath path("heyyyyy");
std::set<AssetCatalogPath> path_std_set;
path_std_set.insert(path);
Set<AssetCatalogPath> path_blender_set;
path_blender_set.add(path);
}
TEST_F(AssetCatalogPathTest, stream_operator)
{
AssetCatalogPath path("путь/в/Пермь");
std::stringstream sstream;
sstream << path;
EXPECT_EQ("путь/в/Пермь", sstream.str());
}
TEST_F(AssetCatalogPathTest, is_contained_in)
{
const AssetCatalogPath catpath("simple/path/child");
EXPECT_FALSE(catpath.is_contained_in("unrelated"));
EXPECT_FALSE(catpath.is_contained_in("sim"));
EXPECT_FALSE(catpath.is_contained_in("simple/pathx"));
EXPECT_FALSE(catpath.is_contained_in("simple/path/c"));
EXPECT_FALSE(catpath.is_contained_in("simple/path/child/grandchild"));
EXPECT_FALSE(catpath.is_contained_in("simple/path/"))
<< "Non-normalized paths are not expected to work.";
EXPECT_TRUE(catpath.is_contained_in(""));
EXPECT_TRUE(catpath.is_contained_in("simple"));
EXPECT_TRUE(catpath.is_contained_in("simple/path"));
/* Test with some UTF8 non-ASCII characters. */
AssetCatalogPath some_parent("some/родитель");
AssetCatalogPath child = some_parent / "ребенок";
EXPECT_TRUE(child.is_contained_in(some_parent));
EXPECT_TRUE(child.is_contained_in("some"));
AssetCatalogPath appended_compound_path = some_parent / "ребенок/внук";
EXPECT_TRUE(appended_compound_path.is_contained_in(some_parent));
EXPECT_TRUE(appended_compound_path.is_contained_in(child));
/* Test "going up" directory-style. */
AssetCatalogPath child_with_dotdot = some_parent / "../../other/hierarchy/part";
EXPECT_TRUE(child_with_dotdot.is_contained_in(some_parent))
<< "dotdot path components should have no meaning";
}
TEST_F(AssetCatalogPathTest, cleanup)
{
{
AssetCatalogPath ugly_path("/ some / родитель / ");
AssetCatalogPath clean_path = ugly_path.cleanup();
EXPECT_EQ(AssetCatalogPath("/ some / родитель / "), ugly_path)
<< "cleanup should not modify the path instance itself";
EXPECT_EQ(AssetCatalogPath("some/родитель"), clean_path);
}
{
AssetCatalogPath double_slashed("some//родитель");
EXPECT_EQ(AssetCatalogPath("some/родитель"), double_slashed.cleanup());
}
{
AssetCatalogPath with_colons("some/key:subkey=value/path");
EXPECT_EQ(AssetCatalogPath("some/key-subkey=value/path"), with_colons.cleanup());
}
{
const AssetCatalogPath with_backslashes("windows\\for\\life");
EXPECT_EQ(AssetCatalogPath("windows/for/life"), with_backslashes.cleanup());
}
{
const AssetCatalogPath with_mixed("windows\\for/life");
EXPECT_EQ(AssetCatalogPath("windows/for/life"), with_mixed.cleanup());
}
{
const AssetCatalogPath with_punctuation("is!/this?/¿valid?");
EXPECT_EQ(AssetCatalogPath("is!/this?/¿valid?"), with_punctuation.cleanup());
}
}
TEST_F(AssetCatalogPathTest, iterate_components)
{
AssetCatalogPath path("путь/в/Пермь");
Vector<std::pair<std::string, bool>> seen_components;
path.iterate_components([&seen_components](StringRef component_name, bool is_last_component) {
std::pair<std::string, bool> parameter_pair = std::make_pair<std::string, bool>(
component_name, bool(is_last_component));
seen_components.append(parameter_pair);
});
ASSERT_EQ(3, seen_components.size());
EXPECT_EQ("путь", seen_components[0].first);
EXPECT_EQ("в", seen_components[1].first);
EXPECT_EQ("Пермь", seen_components[2].first);
EXPECT_FALSE(seen_components[0].second);
EXPECT_FALSE(seen_components[1].second);
EXPECT_TRUE(seen_components[2].second);
}
TEST_F(AssetCatalogPathTest, rebase)
{
AssetCatalogPath path("some/path/to/some/catalog");
EXPECT_EQ(path.rebase("some/path", "new/base"), "new/base/to/some/catalog");
EXPECT_EQ(path.rebase("", "new/base"), "new/base/some/path/to/some/catalog");
EXPECT_EQ(path.rebase("some/path/to/some/catalog", "some/path/to/some/catalog"),
"some/path/to/some/catalog")
<< "Rebasing to itself should not change the path.";
EXPECT_EQ(path.rebase("path/to", "new/base"), "")
<< "Non-matching base path should return empty string to indicate 'NO'.";
/* Empty strings should be handled without crashing or other nasty side-effects. */
AssetCatalogPath empty("");
EXPECT_EQ(empty.rebase("path/to", "new/base"), "");
EXPECT_EQ(empty.rebase("", "new/base"), "new/base");
EXPECT_EQ(empty.rebase("", ""), "");
}
TEST_F(AssetCatalogPathTest, parent)
{
const AssetCatalogPath ascii_path("path/with/missing/parents");
EXPECT_EQ(ascii_path.parent(), "path/with/missing");
const AssetCatalogPath path("путь/в/Пермь/долог/и/далек");
EXPECT_EQ(path.parent(), "путь/в/Пермь/долог/и");
EXPECT_EQ(path.parent().parent(), "путь/в/Пермь/долог");
EXPECT_EQ(path.parent().parent().parent(), "путь/в/Пермь");
const AssetCatalogPath one_level("one");
EXPECT_EQ(one_level.parent(), "");
const AssetCatalogPath empty("");
EXPECT_EQ(empty.parent(), "");
}
} // namespace blender::asset_system::tests

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,161 @@
/* SPDX-FileCopyrightText: 2020 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "AS_asset_catalog.hh"
#include "AS_asset_catalog_tree.hh"
#include "BLI_path_utils.hh"
#include "testing/testing.h"
#include "asset_library_test_common.hh"
namespace blender::asset_system::tests {
class AssetCatalogTreeTest : public AssetLibraryTestBase, public AssetCatalogTreeTestFunctions {};
TEST_F(AssetCatalogTreeTest, insert_item_into_tree)
{
{
AssetCatalogTree tree;
std::unique_ptr<AssetCatalog> catalog_empty_path = AssetCatalog::from_path("");
tree.insert_item(*catalog_empty_path);
expect_tree_items(tree, {});
}
{
AssetCatalogTree tree;
std::unique_ptr<AssetCatalog> catalog = AssetCatalog::from_path("item");
tree.insert_item(*catalog);
expect_tree_items(tree, {"item"});
/* Insert child after parent already exists. */
std::unique_ptr<AssetCatalog> child_catalog = AssetCatalog::from_path("item/child");
tree.insert_item(*catalog);
expect_tree_items(tree, {"item", "item/child"});
std::vector<AssetCatalogPath> expected_paths;
/* Test inserting multi-component sub-path. */
std::unique_ptr<AssetCatalog> grandgrandchild_catalog = AssetCatalog::from_path(
"item/child/grandchild/grandgrandchild");
tree.insert_item(*catalog);
expected_paths = {
"item", "item/child", "item/child/grandchild", "item/child/grandchild/grandgrandchild"};
expect_tree_items(tree, expected_paths);
std::unique_ptr<AssetCatalog> root_level_catalog = AssetCatalog::from_path("root level");
tree.insert_item(*catalog);
expected_paths = {"item",
"item/child",
"item/child/grandchild",
"item/child/grandchild/grandgrandchild",
"root level"};
expect_tree_items(tree, expected_paths);
}
{
AssetCatalogTree tree;
std::unique_ptr<AssetCatalog> catalog = AssetCatalog::from_path("item/child");
tree.insert_item(*catalog);
expect_tree_items(tree, {"item", "item/child"});
}
{
AssetCatalogTree tree;
std::unique_ptr<AssetCatalog> catalog = AssetCatalog::from_path("white space");
tree.insert_item(*catalog);
expect_tree_items(tree, {"white space"});
}
{
AssetCatalogTree tree;
std::unique_ptr<AssetCatalog> catalog = AssetCatalog::from_path("/item/white space");
tree.insert_item(*catalog);
expect_tree_items(tree, {"item", "item/white space"});
}
{
AssetCatalogTree tree;
std::unique_ptr<AssetCatalog> catalog_unicode_path = AssetCatalog::from_path("Ružena");
tree.insert_item(*catalog_unicode_path);
expect_tree_items(tree, {"Ružena"});
catalog_unicode_path = AssetCatalog::from_path("Ružena/Ružena");
tree.insert_item(*catalog_unicode_path);
expect_tree_items(tree, {"Ružena", "Ružena/Ružena"});
}
}
TEST_F(AssetCatalogTreeTest, load_single_file_into_tree)
{
AssetCatalogService service(asset_library_root_);
service.load_from_disk(asset_library_root_ + SEP_STR + "blender_assets.cats.txt");
/* Contains not only paths from the CDF but also the missing parents (implicitly defined
* catalogs). */
std::vector<AssetCatalogPath> expected_paths{
"character",
"character/Ellie",
"character/Ellie/backslashes",
"character/Ellie/poselib",
"character/Ellie/poselib/tailslash",
"character/Ellie/poselib/white space",
"character/Ružena",
"character/Ružena/poselib",
"character/Ružena/poselib/face",
"character/Ružena/poselib/hand",
"path", /* Implicit. */
"path/without", /* Implicit. */
"path/without/simplename", /* From CDF. */
};
const std::shared_ptr<const AssetCatalogTree> tree = service.catalog_tree();
expect_tree_items(*tree, expected_paths);
}
TEST_F(AssetCatalogTreeTest, foreach_in_tree)
{
{
AssetCatalogTree tree{};
const std::vector<AssetCatalogPath> no_catalogs{};
expect_tree_items(tree, no_catalogs);
expect_tree_root_items(tree, no_catalogs);
/* Need a root item to check child items. */
std::unique_ptr<AssetCatalog> catalog = AssetCatalog::from_path("something");
tree.insert_item(*catalog);
tree.foreach_root_item([&no_catalogs](const AssetCatalogTreeItem &item) {
expect_tree_item_child_items(item, no_catalogs);
});
}
AssetCatalogService service(asset_library_root_);
service.load_from_disk(asset_library_root_ + SEP_STR + "blender_assets.cats.txt");
std::vector<AssetCatalogPath> expected_root_items{{"character", "path"}};
const std::shared_ptr<const AssetCatalogTree> tree = service.catalog_tree();
expect_tree_root_items(*tree, expected_root_items);
/* Test if the direct children of the root item are what's expected. */
std::vector<std::vector<AssetCatalogPath>> expected_root_child_items = {
/* Children of the "character" root item. */
{"character/Ellie", "character/Ružena"},
/* Children of the "path" root item. */
{"path/without"},
};
int i = 0;
tree->foreach_root_item([&expected_root_child_items, &i](const AssetCatalogTreeItem &item) {
expect_tree_item_child_items(item, expected_root_child_items[i]);
i++;
});
}
} // namespace blender::asset_system::tests

View File

@@ -0,0 +1,380 @@
/* SPDX-FileCopyrightText: 2020 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "asset_library_service.hh"
#include "BLI_fileops.h" /* For PATH_MAX (at least on Windows). */
#include "BLI_path_utils.hh"
#include "BLI_string.h"
#include "BKE_appdir.hh"
#include "BKE_callbacks.hh"
#include "BKE_gtest_base.hh"
#include "BKE_main.hh"
#include "DNA_asset_types.h"
#include "CLG_log.h"
#include "testing/testing.h"
namespace blender::asset_system::tests {
const UUID UUID_POSES_ELLIE("df60e1f6-2259-475b-93d9-69a1b4a8db78");
class AssetLibraryServiceTest : public bke::BlenderGTestBase {
public:
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 + SEP_STR + "asset_library";
temp_library_path_ = "";
}
void TearDown() override
{
AssetLibraryService::destroy();
if (!temp_library_path_.empty()) {
BLI_delete(temp_library_path_.c_str(), true, true);
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(nullptr);
const CatalogFilePath tempdir = BKE_tempdir_session();
temp_library_path_ = tempdir + "test-temporary-path" + SEP_STR;
return temp_library_path_;
}
CatalogFilePath create_temp_path()
{
CatalogFilePath path = use_temp_path();
BLI_dir_create_recursive(path.c_str());
return path;
}
};
TEST_F(AssetLibraryServiceTest, get_destroy)
{
AssetLibraryService *const service = AssetLibraryService::get();
EXPECT_EQ(service, AssetLibraryService::get())
<< "Calling twice without destroying in between should return the same instance.";
/* This should not crash. */
AssetLibraryService::destroy();
AssetLibraryService::destroy();
/* NOTE: there used to be a test for the opposite here, that after a call to
* AssetLibraryService::destroy() the above calls should return freshly allocated objects. This
* cannot be reliably tested by just pointer comparison, though. */
}
TEST_F(AssetLibraryServiceTest, library_pointers)
{
AssetLibraryService *service = AssetLibraryService::get();
AssetLibrary *const lib = service->get_asset_library_on_disk_custom(__func__,
asset_library_root_);
AssetLibrary *const curfile_lib = service->get_asset_library_current_file();
EXPECT_EQ(lib, service->get_asset_library_on_disk_custom(__func__, asset_library_root_))
<< "Calling twice without destroying in between should return the same instance.";
EXPECT_EQ(curfile_lib, service->get_asset_library_current_file())
<< "Calling twice without destroying in between should return the same instance.";
/* NOTE: there used to be a test for the opposite here, that after a call to
* AssetLibraryService::destroy() the above calls should return freshly allocated objects. This
* cannot be reliably tested by just pointer comparison, though. */
}
TEST_F(AssetLibraryServiceTest, library_from_reference)
{
AssetLibraryService *service = AssetLibraryService::get();
AssetLibrary *const curfile_lib = service->get_asset_library_current_file();
AssetLibraryReference ref{};
ref.type = ASSET_LIBRARY_LOCAL;
EXPECT_EQ(curfile_lib, service->get_asset_library(nullptr, ref))
<< "Getting the local (current file) reference without a main saved on disk should return "
"the current file library";
{
Main dummy_main{};
std::string dummy_filepath = asset_library_root_ + SEP + "dummy.blend";
STRNCPY(dummy_main.filepath, dummy_filepath.c_str());
AssetLibrary *custom_lib = service->get_asset_library_on_disk_custom(__func__,
asset_library_root_);
AssetLibrary *tmp_curfile_lib = service->get_asset_library(&dummy_main, ref);
/* Requested a current file library with a (fake) file saved in the same directory as a custom
* asset library. The resulting library should never match the custom asset library, even
* though the paths match. */
EXPECT_NE(custom_lib, tmp_curfile_lib)
<< "Getting an asset library from a local (current file) library reference should never "
"match any custom asset library";
EXPECT_EQ(custom_lib->root_path(), tmp_curfile_lib->root_path());
}
}
TEST_F(AssetLibraryServiceTest, library_path_trailing_slashes)
{
AssetLibraryService *service = AssetLibraryService::get();
char asset_lib_no_slash[PATH_MAX];
char asset_lib_with_slash[PATH_MAX];
STRNCPY(asset_lib_no_slash, asset_library_root_.c_str());
STRNCPY(asset_lib_with_slash, asset_library_root_.c_str());
/* Ensure #asset_lib_no_slash has no trailing slash, regardless of what was passed on the CLI to
* the unit test. */
while (strlen(asset_lib_no_slash) &&
ELEM(asset_lib_no_slash[strlen(asset_lib_no_slash) - 1], SEP, ALTSEP))
{
asset_lib_no_slash[strlen(asset_lib_no_slash) - 1] = '\0';
}
BLI_path_slash_ensure(asset_lib_with_slash, PATH_MAX);
AssetLibrary *const lib_no_slash = service->get_asset_library_on_disk_custom(__func__,
asset_lib_no_slash);
EXPECT_EQ(lib_no_slash,
service->get_asset_library_on_disk_custom(__func__, asset_lib_with_slash))
<< "With or without trailing slash shouldn't matter.";
}
TEST_F(AssetLibraryServiceTest, catalogs_loaded)
{
AssetLibraryService *const service = AssetLibraryService::get();
AssetLibrary *const lib = service->get_asset_library_on_disk_custom(__func__,
asset_library_root_);
AssetCatalogService &cat_service = lib->catalog_service();
const UUID UUID_POSES_ELLIE("df60e1f6-2259-475b-93d9-69a1b4a8db78");
EXPECT_NE(nullptr, cat_service.find_catalog(UUID_POSES_ELLIE))
<< "Catalogs should be loaded after getting an asset library from disk.";
}
TEST_F(AssetLibraryServiceTest, has_any_unsaved_catalogs)
{
AssetLibraryService *const service = AssetLibraryService::get();
EXPECT_FALSE(service->has_any_unsaved_catalogs())
<< "Empty AssetLibraryService should have no unsaved catalogs";
AssetLibrary *const lib = service->get_asset_library_on_disk_custom(__func__,
asset_library_root_);
AssetCatalogService &cat_service = lib->catalog_service();
EXPECT_FALSE(service->has_any_unsaved_catalogs())
<< "Unchanged AssetLibrary should have no unsaved catalogs";
const UUID UUID_POSES_ELLIE("df60e1f6-2259-475b-93d9-69a1b4a8db78");
cat_service.prune_catalogs_by_id(UUID_POSES_ELLIE);
EXPECT_FALSE(service->has_any_unsaved_catalogs())
<< "Deletion of catalogs via AssetCatalogService should not automatically tag as 'unsaved "
"changes'.";
const UUID UUID_POSES_RUZENA("79a4f887-ab60-4bd4-94da-d572e27d6aed");
AssetCatalog *cat = cat_service.find_catalog(UUID_POSES_RUZENA);
ASSERT_NE(nullptr, cat) << "Catalog " << UUID_POSES_RUZENA << " should be known";
cat_service.tag_has_unsaved_changes(cat);
EXPECT_TRUE(service->has_any_unsaved_catalogs())
<< "Tagging as having unsaved changes of a single catalog service should result in unsaved "
"changes being reported.";
EXPECT_TRUE(cat->flags.has_unsaved_changes);
}
TEST_F(AssetLibraryServiceTest, has_any_unsaved_catalogs_after_write)
{
const CatalogFilePath writable_dir = create_temp_path(); /* Has trailing slash. */
const CatalogFilePath original_cdf_file = asset_library_root_ + SEP_STR +
"blender_assets.cats.txt";
CatalogFilePath writable_cdf_file = writable_dir + AssetCatalogService::DEFAULT_CATALOG_FILENAME;
BLI_path_slash_native(writable_cdf_file.data());
ASSERT_EQ(0, BLI_copy(original_cdf_file.c_str(), writable_cdf_file.c_str()));
AssetLibraryService *const service = AssetLibraryService::get();
AssetLibrary *const lib = service->get_asset_library_on_disk_custom(__func__, writable_dir);
EXPECT_FALSE(service->has_any_unsaved_catalogs())
<< "Unchanged AssetLibrary should have no unsaved catalogs";
AssetCatalogService &cat_service = lib->catalog_service();
AssetCatalog *cat = cat_service.find_catalog(UUID_POSES_ELLIE);
cat_service.tag_has_unsaved_changes(cat);
EXPECT_TRUE(service->has_any_unsaved_catalogs())
<< "Tagging as having unsaved changes of a single catalog service should result in unsaved "
"changes being reported.";
EXPECT_TRUE(cat->flags.has_unsaved_changes);
cat_service.write_to_disk(writable_dir + "dummy_path.blend");
EXPECT_FALSE(service->has_any_unsaved_catalogs())
<< "Written AssetCatalogService should have no unsaved catalogs";
EXPECT_FALSE(cat->flags.has_unsaved_changes);
}
/**
* Call #AssetLibraryService::move_runtime_current_file_into_on_disk_library() with an on disk
* location that contains no existing asset catalog definition file.
*/
TEST_F(AssetLibraryServiceTest, move_runtime_current_file_into_on_disk_library__empty_directory)
{
AssetLibraryService *service = AssetLibraryService::get();
AssetLibrary *runtime_lib = service->get_asset_library_current_file();
AssetCatalogService &runtime_catservice = runtime_lib->catalog_service();
/* Catalog created in the runtime lib that should be moved to the on-disk lib. */
AssetCatalog *catalog = runtime_catservice.create_catalog("Some/Catalog/Path");
runtime_catservice.undo_push();
{
EXPECT_TRUE(catalog->flags.has_unsaved_changes);
EXPECT_EQ(nullptr, runtime_catservice.find_catalog(UUID_POSES_ELLIE))
<< "Catalog not expected in the runtime asset library.";
}
{
Main dummy_main{};
std::string dummy_filepath = create_temp_path() + "dummy.blend";
STRNCPY(dummy_main.filepath, dummy_filepath.c_str());
AssetLibraryService::move_runtime_current_file_into_on_disk_library(dummy_main);
AssetLibraryReference ref{};
ref.type = ASSET_LIBRARY_LOCAL;
/* Loads and merges the catalogs from disk. */
AssetLibrary *on_disk_lib = service->get_asset_library(&dummy_main, ref);
AssetCatalogService &on_disk_catservice = on_disk_lib->catalog_service();
/* Can only test the pointer equality here because the implementation keeps the runtime library
* alive until all its contents are moved to the on-disk library. Otherwise the allocator might
* choose the same address for the new on-disk library. Useful for testing, though not
* required. */
EXPECT_NE(on_disk_lib, runtime_lib);
EXPECT_EQ(on_disk_lib->root_path(), temp_library_path_);
/* Check if catalog was moved correctly. */
{
EXPECT_EQ(on_disk_catservice.find_catalog(catalog->catalog_id)->path, catalog->path);
/* Compare catalog by pointer. #move_runtime_current_file_into_on_disk_library() doesn't
* guarantee publicly that catalog pointers remain unchanged, but practically code might rely
* on it. Good to know if this breaks. */
EXPECT_EQ(on_disk_catservice.find_catalog(catalog->catalog_id), catalog);
/* No writing happened, just merging. */
EXPECT_TRUE(on_disk_catservice.find_catalog(catalog->catalog_id)->flags.has_unsaved_changes);
}
EXPECT_EQ(nullptr, runtime_catservice.find_catalog(UUID_POSES_ELLIE))
<< "Catalog not expected in the on disk asset library.";
/* Check if undo stack was moved correctly. */
{
on_disk_catservice.undo();
const AssetCatalog *ellie_catalog = on_disk_catservice.find_catalog(UUID_POSES_ELLIE);
EXPECT_EQ(nullptr, ellie_catalog) << "This catalog should not be present after undo";
EXPECT_EQ(on_disk_catservice.find_catalog(catalog->catalog_id)->path, catalog->path)
<< "This catalog should still be present after undo";
}
/* Force a new current file runtime library to be created. */
EXPECT_NE(service->get_asset_library_current_file(), on_disk_lib);
}
}
/**
* Call #AssetLibraryService::move_runtime_current_file_into_on_disk_library() with an on disk
* location that contains an existing asset catalog definition file.
* Result should be merged libraries.
*/
TEST_F(AssetLibraryServiceTest,
move_runtime_current_file_into_on_disk_library__directory_with_catalogs)
{
AssetLibraryService *service = AssetLibraryService::get();
AssetLibrary *runtime_lib = service->get_asset_library_current_file();
AssetCatalogService &runtime_catservice = runtime_lib->catalog_service();
/* Catalog created in the runtime lib that should be moved to the on-disk lib. */
AssetCatalog *catalog = runtime_catservice.create_catalog("Some/Catalog/Path");
runtime_catservice.undo_push();
{
EXPECT_TRUE(catalog->flags.has_unsaved_changes);
EXPECT_EQ(nullptr, runtime_catservice.find_catalog(UUID_POSES_ELLIE))
<< "Catalog not expected in the runtime asset library.";
}
{
Main dummy_main{};
std::string dummy_filepath = asset_library_root_ + SEP + "dummy.blend";
STRNCPY(dummy_main.filepath, dummy_filepath.c_str());
AssetLibraryService::move_runtime_current_file_into_on_disk_library(dummy_main);
AssetLibraryReference ref{};
ref.type = ASSET_LIBRARY_LOCAL;
/* Loads and merges the catalogs from disk. */
AssetLibrary *on_disk_lib = service->get_asset_library(&dummy_main, ref);
AssetCatalogService &on_disk_catservice = on_disk_lib->catalog_service();
EXPECT_NE(on_disk_lib, runtime_lib);
EXPECT_EQ(BLI_path_cmp_normalized(on_disk_lib->root_path().c_str(),
(asset_library_root_ + SEP).c_str()),
0);
/* Check if catalog was moved correctly. */
{
EXPECT_EQ(on_disk_catservice.find_catalog(catalog->catalog_id)->path, catalog->path);
/* Compare catalog by pointer. #move_runtime_current_file_into_on_disk_library() doesn't
* guarantee publicly that catalog pointers remain unchanged, but practically code might rely
* on it. Good to know if this breaks. */
EXPECT_EQ(on_disk_catservice.find_catalog(catalog->catalog_id), catalog);
/* No writing happened, just merging. */
EXPECT_TRUE(on_disk_catservice.find_catalog(catalog->catalog_id)->flags.has_unsaved_changes);
}
/* Check if catalogs have been merged in from disk correctly (by #get_asset_library()). */
{
const AssetCatalog *ellie_catalog = on_disk_catservice.find_catalog(UUID_POSES_ELLIE);
EXPECT_NE(nullptr, ellie_catalog)
<< "Catalogs should be loaded after getting an asset library from disk.";
EXPECT_FALSE(ellie_catalog->flags.has_unsaved_changes);
}
/* Check if undo stack was moved correctly. */
{
on_disk_catservice.undo();
const AssetCatalog *ellie_catalog = on_disk_catservice.find_catalog(UUID_POSES_ELLIE);
EXPECT_EQ(nullptr, ellie_catalog) << "This catalog should not be present after undo";
EXPECT_EQ(on_disk_catservice.find_catalog(catalog->catalog_id)->path, catalog->path)
<< "This catalog should still be present after undo";
}
/* Force a new current file runtime library to be created. */
EXPECT_NE(service->get_asset_library_current_file(), on_disk_lib);
}
}
} // namespace blender::asset_system::tests

View File

@@ -0,0 +1,67 @@
/* SPDX-FileCopyrightText: 2020 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "AS_asset_catalog.hh"
#include "AS_asset_library.hh"
#include "BKE_gtest_base.hh"
#include "asset_library_service.hh"
#include "testing/testing.h"
namespace blender::asset_system::tests {
class AssetLibraryTest : public bke::BlenderGTestBase {
public:
void TearDown() override
{
asset_system::AssetLibraryService::destroy();
}
};
TEST_F(AssetLibraryTest, AS_asset_library_load_from_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_dirpath = test_files_dir + "/" + "asset_library";
AssetLibrary *library = AS_asset_library_load_from_directory(__func__, library_dirpath.data());
ASSERT_NE(nullptr, library);
/* Check that it can be cast to the C++ type and has a Catalog Service. */
const AssetCatalogService &service = library->catalog_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 loading went OK is for
* the asset catalog service tests. */
const UUID 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.str());
}
TEST_F(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_dirpath = test_files_dir + "/" +
"asset_library/this/subdir/does/not/exist";
AssetLibrary *library = AS_asset_library_load_from_directory(__func__, library_dirpath.data());
ASSERT_NE(nullptr, library);
/* Check that it can be cast to the C++ type and has a Catalog Service. */
AssetCatalogService &service = library->catalog_service();
/* Check that the catalog service doesn't have any catalogs. */
EXPECT_TRUE(service.is_empty());
}
} // namespace blender::asset_system::tests

View File

@@ -0,0 +1,166 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: Apache-2.0 */
#pragma once
#include <string>
#include <vector>
#include "AS_asset_catalog.hh"
#include "AS_asset_catalog_tree.hh"
#include "asset_library_service.hh"
#include "BKE_appdir.hh"
#include "BKE_gtest_base.hh"
#include "BLI_fileops.h"
#include "BLI_path_utils.hh"
#include "testing/testing.h"
namespace blender {
namespace asset_system {
class AssetCatalogTree;
class AssetCatalogTreeItem;
class AssetCatalogPath;
} // namespace asset_system
namespace asset_system::tests {
/**
* Functionality to setup and access directories on disk within which asset library related testing
* can be done.
*/
class AssetLibraryTestBase : public bke::BlenderGTestBase {
protected:
std::string asset_library_root_;
std::string 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 + SEP_STR + "asset_library";
temp_library_path_ = "";
}
void TearDown() override
{
AssetLibraryService::destroy();
if (!temp_library_path_.empty()) {
BLI_delete(temp_library_path_.c_str(), true, true);
temp_library_path_ = "";
}
}
/* Register a temporary path, which will be removed at the end of the test.
* The returned path ends in a slash. */
std::string use_temp_path()
{
BKE_tempdir_init(nullptr);
const std::string tempdir = BKE_tempdir_session();
temp_library_path_ = tempdir + "test-temporary-path" + SEP_STR;
return temp_library_path_;
}
std::string create_temp_path()
{
std::string path = use_temp_path();
BLI_dir_create_recursive(path.c_str());
return path;
}
};
class AssetCatalogTreeTestFunctions {
public:
/**
* Recursively iterate over all tree items using #AssetCatalogTree::foreach_item() and check if
* the items map exactly to \a expected_paths.
*/
static void expect_tree_items(const AssetCatalogTree &tree,
const std::vector<AssetCatalogPath> &expected_paths);
/**
* Iterate over the root items of \a tree and check if the items map exactly to \a
* expected_paths. Similar to #assert_expected_tree_items() but calls
* #AssetCatalogTree::foreach_root_item() instead of #AssetCatalogTree::foreach_item().
*/
static void expect_tree_root_items(const AssetCatalogTree &tree,
const std::vector<AssetCatalogPath> &expected_paths);
/**
* Iterate over the child items of \a parent_item and check if the items map exactly to \a
* expected_paths. Similar to #assert_expected_tree_items() but calls
* #AssetCatalogTreeItem::foreach_child() instead of #AssetCatalogTree::foreach_item().
*/
static void expect_tree_item_child_items(const AssetCatalogTreeItem &parent_item,
const std::vector<AssetCatalogPath> &expected_paths);
};
static inline void compare_item_with_path(const AssetCatalogPath &expected_path,
const AssetCatalogTreeItem &actual_item)
{
if (expected_path != actual_item.catalog_path().str()) {
/* This will fail, but with a nicer error message than just calling FAIL(). */
EXPECT_EQ(expected_path, actual_item.catalog_path());
return;
}
/* Is the catalog name as expected? "character", "Ellie", ... */
EXPECT_EQ(expected_path.name(), actual_item.get_name());
/* Does the computed number of parents match? */
const std::string expected_path_str = expected_path.str();
const size_t expected_parent_count = std::count(
expected_path_str.begin(), expected_path_str.end(), AssetCatalogPath::SEPARATOR);
EXPECT_EQ(expected_parent_count, actual_item.count_parents());
}
inline void AssetCatalogTreeTestFunctions::expect_tree_items(
const AssetCatalogTree &tree, const std::vector<AssetCatalogPath> &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();
compare_item_with_path(expected_paths[i], actual_item);
i++;
});
}
inline void AssetCatalogTreeTestFunctions::expect_tree_root_items(
const AssetCatalogTree &tree, const std::vector<AssetCatalogPath> &expected_paths)
{
int i = 0;
tree.foreach_root_item([&](const AssetCatalogTreeItem &actual_item) {
ASSERT_LT(i, expected_paths.size())
<< "More catalogs in tree root than expected; did not expect "
<< actual_item.catalog_path();
compare_item_with_path(expected_paths[i], actual_item);
i++;
});
}
inline void AssetCatalogTreeTestFunctions::expect_tree_item_child_items(
const AssetCatalogTreeItem &parent_item, const std::vector<AssetCatalogPath> &expected_paths)
{
int i = 0;
parent_item.foreach_child([&](const AssetCatalogTreeItem &actual_item) {
ASSERT_LT(i, expected_paths.size())
<< "More catalogs in tree item than expected; did not expect "
<< actual_item.catalog_path();
compare_item_with_path(expected_paths[i], actual_item);
i++;
});
}
} // namespace asset_system::tests
} // namespace blender

View File

@@ -0,0 +1,360 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "asset_library_service.hh"
#include "asset_library_test_common.hh"
#include "AS_asset_representation.hh"
#include "BKE_lib_id.hh"
#include "BKE_main.hh"
#if defined(WIN32)
# include "BLI_string.h"
#endif
#include "DNA_asset_types.h"
#include "DNA_object_types.h"
#include "ED_asset_mark_clear.hh"
#include "../intern/utils.hh"
#include "testing/testing.h"
namespace blender::asset_system::tests {
/**
* Sets up asset library loading so we have a library to load asset representations into (required
* for some functionality to perform work).
*/
class AssetRepresentationTest : public AssetLibraryTestBase {
public:
AssetLibrary *get_builtin_library_from_type(eAssetLibraryType type)
{
AssetLibraryService *service = AssetLibraryService::get();
AssetLibraryReference ref{};
ref.type = type;
return service->get_asset_library(nullptr, ref);
}
AssetRepresentation &add_dummy_asset(AssetLibrary &library, StringRef relative_path)
{
std::unique_ptr<AssetMetaData> dummy_metadata = std::make_unique<AssetMetaData>();
return *library
.add_external_on_disk_asset(
relative_path, "Some asset name", 0, std::move(dummy_metadata))
.lock();
}
AssetRepresentation &add_dummy_id_asset(AssetLibrary &library, ID &id)
{
/* Ensure ID is marked as asset (no-op if already marked). */
ed::asset::mark_id(&id);
return *library.add_local_id_asset(id).lock();
}
};
TEST_F(AssetRepresentationTest, library_relative_identifier__id_name_change)
{
Main *bmain = BKE_main_new();
Object *object = BKE_id_new<Object>(bmain, "Before rename");
AssetLibrary *library = get_builtin_library_from_type(ASSET_LIBRARY_LOCAL);
AssetRepresentation &asset = add_dummy_id_asset(*library, object->id);
EXPECT_EQ(asset.library_relative_identifier(), "Object" SEP_STR "Before rename");
BKE_id_rename(*bmain, object->id, "Renamed!");
EXPECT_EQ(asset.library_relative_identifier(), "Object" SEP_STR "Renamed!");
BKE_id_rename(*bmain, object->id, "Name/With\\Slashes/");
EXPECT_EQ(asset.library_relative_identifier(), "Object" SEP_STR "Name/With\\Slashes/");
BKE_main_free(bmain);
}
TEST_F(AssetRepresentationTest, weak_reference__current_file)
{
AssetLibrary *library = get_builtin_library_from_type(ASSET_LIBRARY_LOCAL);
AssetRepresentation &asset = add_dummy_asset(*library, "path/to/an/asset");
{
AssetWeakReference weak_ref = asset.make_weak_reference();
EXPECT_EQ(weak_ref.asset_library_type, ASSET_LIBRARY_LOCAL);
EXPECT_EQ(weak_ref.asset_library_identifier, nullptr);
EXPECT_STREQ(weak_ref.relative_asset_identifier, "path/to/an/asset");
}
}
TEST_F(AssetRepresentationTest, weak_reference__custom_library)
{
AssetLibraryService *service = AssetLibraryService::get();
AssetLibrary *const library = service->get_asset_library_on_disk_custom("My custom lib",
asset_library_root_);
AssetRepresentation &asset = add_dummy_asset(*library, "path/to/an/asset");
{
AssetWeakReference weak_ref = asset.make_weak_reference();
EXPECT_EQ(weak_ref.asset_library_type, ASSET_LIBRARY_CUSTOM);
EXPECT_STREQ(weak_ref.asset_library_identifier, "My custom lib");
EXPECT_STREQ(weak_ref.relative_asset_identifier, "path/to/an/asset");
}
}
/* Test if new weak references the ID name changes. */
TEST_F(AssetRepresentationTest, weak_reference__id_name_change)
{
Main *bmain = BKE_main_new();
Object *object = BKE_id_new<Object>(bmain, "Before rename");
AssetLibrary *library = get_builtin_library_from_type(ASSET_LIBRARY_LOCAL);
AssetRepresentation &asset = add_dummy_id_asset(*library, object->id);
{
AssetWeakReference weak_ref = asset.make_weak_reference();
EXPECT_EQ(weak_ref.asset_library_type, ASSET_LIBRARY_LOCAL);
EXPECT_STREQ(weak_ref.asset_library_identifier, nullptr);
EXPECT_STREQ(weak_ref.relative_asset_identifier, "Object" SEP_STR "Before rename");
}
BKE_id_rename(*bmain, object->id, "Renamed!");
{
AssetWeakReference weak_ref = asset.make_weak_reference();
EXPECT_EQ(weak_ref.asset_library_type, ASSET_LIBRARY_LOCAL);
EXPECT_STREQ(weak_ref.asset_library_identifier, nullptr);
EXPECT_STREQ(weak_ref.relative_asset_identifier, "Object" SEP_STR "Renamed!");
}
BKE_id_rename(*bmain, object->id, "Name/With\\Slashes/");
{
AssetWeakReference weak_ref = asset.make_weak_reference();
EXPECT_EQ(weak_ref.asset_library_type, ASSET_LIBRARY_LOCAL);
EXPECT_STREQ(weak_ref.asset_library_identifier, nullptr);
EXPECT_STREQ(weak_ref.relative_asset_identifier, "Object" SEP_STR "Name/With\\Slashes/");
}
BKE_main_free(bmain);
}
TEST_F(AssetRepresentationTest, weak_reference__compare)
{
{
AssetWeakReference a;
AssetWeakReference b;
EXPECT_EQ(a, b);
/* Arbitrary individual member changes to test how it affects the comparison. */
b.asset_library_identifier = "My lib";
/* Asset library identifier should be ignored unless the type is #ASSET_LIBRARY_CUSTOM. */
EXPECT_EQ(a, b);
a.asset_library_identifier = "My lib";
EXPECT_EQ(a, b);
a.asset_library_type = ASSET_LIBRARY_ESSENTIALS;
EXPECT_NE(a, b);
b.asset_library_type = ASSET_LIBRARY_LOCAL;
EXPECT_NE(a, b);
b.asset_library_type = ASSET_LIBRARY_ESSENTIALS;
EXPECT_EQ(a, b);
a.relative_asset_identifier = "Foo";
EXPECT_NE(a, b);
b.relative_asset_identifier = "Bar";
EXPECT_NE(a, b);
a.relative_asset_identifier = "Bar";
EXPECT_EQ(a, b);
/* Make the destructor work. */
a.asset_library_identifier = b.asset_library_identifier = nullptr;
a.relative_asset_identifier = b.relative_asset_identifier = nullptr;
}
{
AssetWeakReference a;
a.asset_library_type = ASSET_LIBRARY_LOCAL;
a.asset_library_identifier = "My custom lib";
a.relative_asset_identifier = "path/to/an/asset";
AssetWeakReference b;
EXPECT_NE(a, b);
b.asset_library_type = ASSET_LIBRARY_LOCAL;
b.asset_library_identifier = "My custom lib";
b.relative_asset_identifier = "path/to/an/asset";
EXPECT_EQ(a, b);
/* Make the destructor work. */
a.asset_library_identifier = b.asset_library_identifier = nullptr;
a.relative_asset_identifier = b.relative_asset_identifier = nullptr;
}
{
AssetLibraryService *service = AssetLibraryService::get();
AssetLibrary *const library = service->get_asset_library_on_disk_custom("My custom lib",
asset_library_root_);
AssetRepresentation &asset = add_dummy_asset(*library, "path/to/an/asset");
AssetWeakReference weak_ref = asset.make_weak_reference();
AssetWeakReference other;
other.asset_library_type = ASSET_LIBRARY_CUSTOM;
other.asset_library_identifier = "My custom lib";
other.relative_asset_identifier = "path/to/an/asset";
EXPECT_EQ(weak_ref, other);
other.relative_asset_identifier = "";
EXPECT_NE(weak_ref, other);
other.relative_asset_identifier = nullptr;
EXPECT_NE(weak_ref, other);
/* Make the destructor work. */
other.asset_library_identifier = nullptr;
other.relative_asset_identifier = nullptr;
}
/* Same but comparing windows and unix style paths. */
{
AssetLibraryService *service = AssetLibraryService::get();
AssetLibrary *const library = service->get_asset_library_on_disk_custom("My custom lib",
asset_library_root_);
AssetRepresentation &asset = add_dummy_asset(*library, "path/to/an/asset");
AssetWeakReference weak_ref = asset.make_weak_reference();
AssetWeakReference other;
other.asset_library_type = ASSET_LIBRARY_CUSTOM;
other.asset_library_identifier = "My custom lib";
other.relative_asset_identifier = "path\\to\\an\\asset";
EXPECT_EQ(weak_ref, other);
other.relative_asset_identifier = "";
EXPECT_NE(weak_ref, other);
other.relative_asset_identifier = nullptr;
EXPECT_NE(weak_ref, other);
/* Make the destructor work. */
other.asset_library_identifier = nullptr;
other.relative_asset_identifier = nullptr;
}
}
TEST_F(AssetRepresentationTest, weak_reference__resolve_to_full_path__current_file)
{
AssetLibraryService *service = AssetLibraryService::get();
AssetLibrary *library = get_builtin_library_from_type(ASSET_LIBRARY_LOCAL);
AssetRepresentation &asset = add_dummy_asset(*library, "path/to/an/asset");
AssetWeakReference weak_ref = asset.make_weak_reference();
std::string resolved_path = service->resolve_asset_weak_reference_to_full_path(weak_ref);
EXPECT_EQ(resolved_path, "");
}
/* #AssetLibraryService::resolve_asset_weak_reference_to_full_path(). */
TEST_F(AssetRepresentationTest, weak_reference__resolve_to_full_path__custom_library)
{
AssetLibraryService *service = AssetLibraryService::get();
AssetLibrary *const library = service->get_asset_library_on_disk_custom("My custom lib",
asset_library_root_);
AssetRepresentation &asset = add_dummy_asset(*library, "path/to/an/asset");
AssetWeakReference weak_ref = asset.make_weak_reference();
std::string expected_path = utils::normalize_path(asset_library_root_ + "/" + "path/") +
"to/an/asset";
std::string resolved_path = service->resolve_asset_weak_reference_to_full_path(weak_ref);
EXPECT_EQ(BLI_path_cmp(resolved_path.c_str(), expected_path.c_str()), 0);
}
TEST_F(AssetRepresentationTest,
weak_reference__resolve_to_full_path__custom_library__windows_pathsep)
{
AssetLibraryService *service = AssetLibraryService::get();
AssetLibrary *const library = service->get_asset_library_on_disk_custom("My custom lib",
asset_library_root_);
AssetRepresentation &asset = add_dummy_asset(*library, "path\\to\\an\\asset");
AssetWeakReference weak_ref = asset.make_weak_reference();
std::string expected_path = utils::normalize_path(asset_library_root_ + "\\" + "path\\") +
"to\\an\\asset";
std::string resolved_path = service->resolve_asset_weak_reference_to_full_path(weak_ref);
EXPECT_EQ(BLI_path_cmp(resolved_path.c_str(), expected_path.c_str()), 0);
}
/* #AssetLibraryService::resolve_asset_weak_reference_to_exploded_path(). */
TEST_F(AssetRepresentationTest, weak_reference__resolve_to_exploded_path__current_file)
{
AssetLibraryService *service = AssetLibraryService::get();
AssetLibrary *library = get_builtin_library_from_type(ASSET_LIBRARY_LOCAL);
AssetRepresentation &asset = add_dummy_asset(*library, "path/to/an/asset");
AssetWeakReference weak_ref = asset.make_weak_reference();
std::string expected_full_path = utils::normalize_path("path/to/an/asset", 5);
std::optional<AssetLibraryService::ExplodedPath> resolved_path =
service->resolve_asset_weak_reference_to_exploded_path(weak_ref);
EXPECT_EQ(*resolved_path->full_path, expected_full_path);
EXPECT_EQ(resolved_path->dir_component, "");
EXPECT_EQ(resolved_path->group_component, "path");
/* ID names may contain slashes. */
EXPECT_EQ(resolved_path->name_component, "to/an/asset");
}
/* #AssetLibraryService::resolve_asset_weak_reference_to_exploded_path(). */
TEST_F(AssetRepresentationTest, weak_reference__resolve_to_exploded_path__custom_library)
{
AssetLibraryService *service = AssetLibraryService::get();
AssetLibrary *const library = service->get_asset_library_on_disk_custom("My custom lib",
asset_library_root_);
AssetRepresentation &asset = add_dummy_asset(*library, "some.blend/Material/asset/name");
AssetWeakReference weak_ref = asset.make_weak_reference();
std::string expected_full_path = utils::normalize_path(asset_library_root_ +
"/some.blend/Material/") +
"asset/name";
std::optional<AssetLibraryService::ExplodedPath> resolved_path =
service->resolve_asset_weak_reference_to_exploded_path(weak_ref);
EXPECT_EQ(BLI_path_cmp(resolved_path->full_path->c_str(), expected_full_path.c_str()), 0);
EXPECT_EQ(BLI_path_cmp_normalized(std::string(resolved_path->dir_component).c_str(),
std::string(asset_library_root_ + "/some.blend").c_str()),
0);
EXPECT_EQ(resolved_path->group_component, "Material");
/* ID names may contain slashes. */
EXPECT_EQ(resolved_path->name_component, "asset/name");
}
/* #AssetLibraryService::resolve_asset_weak_reference_to_exploded_path(). */
TEST_F(AssetRepresentationTest,
weak_reference__resolve_to_exploded_path__custom_library__windows_pathsep)
{
AssetLibraryService *service = AssetLibraryService::get();
AssetLibrary *const library = service->get_asset_library_on_disk_custom("My custom lib",
asset_library_root_);
AssetRepresentation &asset = add_dummy_asset(*library, "some.blend\\Material\\asset/name");
AssetWeakReference weak_ref = asset.make_weak_reference();
std::string expected_full_path = utils::normalize_path(asset_library_root_ +
"\\some.blend\\Material\\") +
"asset/name";
std::optional<AssetLibraryService::ExplodedPath> resolved_path =
service->resolve_asset_weak_reference_to_exploded_path(weak_ref);
EXPECT_EQ(BLI_path_cmp(resolved_path->full_path->c_str(), expected_full_path.c_str()), 0);
EXPECT_EQ(BLI_path_cmp_normalized(std::string(resolved_path->dir_component).c_str(),
std::string(asset_library_root_ + "\\some.blend").c_str()),
0);
EXPECT_EQ(resolved_path->group_component, "Material");
/* ID names may contain slashes. */
EXPECT_EQ(resolved_path->name_component, "asset/name");
}
} // namespace blender::asset_system::tests

View File

@@ -0,0 +1,31 @@
/* SPDX-FileCopyrightText: 2026 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "testing/testing.h"
#include "AS_essentials_library.hh"
namespace blender::asset_system::tests {
TEST(EssentialsLibraryTest, is_online_essentials_url)
{
EXPECT_FALSE(is_online_essentials_url(""));
EXPECT_FALSE(is_online_essentials_url("https://www.blender.org/asset-library/"));
EXPECT_TRUE(
is_online_essentials_url("https://cdn.extensions.blender.org/asset-libraries/essentials/"));
EXPECT_TRUE(is_online_essentials_url(
"https://cdn.extensions.blender.org/asset-libraries/essentials/_asset-library-meta.json"));
EXPECT_FALSE(
is_online_essentials_url("https://cdn.extensions.blender.org/asset-libraries/essentials"));
EXPECT_FALSE(is_online_essentials_url(
"https://cdn.extensions.blender.org/asset-libraries/essentials_asset-library-meta.json"));
/* http instead of https. */
EXPECT_FALSE(
is_online_essentials_url("http://cdn.extensions.blender.org/asset-libraries/essentials/"));
}
} // namespace blender::asset_system::tests

View File

@@ -0,0 +1,29 @@
/* SPDX-FileCopyrightText: 2026 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "testing/testing.h"
#include "AS_remote_library.hh"
namespace blender::asset_system::tests {
TEST(RemoteLibraryTest, url_ends_with_top_meta_file_name)
{
EXPECT_FALSE(remote_library_url_ends_with_top_meta_file_name(""));
EXPECT_FALSE(remote_library_url_ends_with_top_meta_file_name("_asset-library-meta.json"));
EXPECT_TRUE(remote_library_url_ends_with_top_meta_file_name(
"https://example.com/_asset-library-meta.json"));
EXPECT_FALSE(remote_library_url_ends_with_top_meta_file_name("https://example.com/"));
EXPECT_FALSE(remote_library_url_ends_with_top_meta_file_name("https://example.com/abc"));
EXPECT_FALSE(remote_library_url_ends_with_top_meta_file_name("https://example.com/abc/"));
EXPECT_FALSE(remote_library_url_ends_with_top_meta_file_name(
"https://example.com/_asset-library-meta.json/"));
/* Missing slash. */
EXPECT_FALSE(remote_library_url_ends_with_top_meta_file_name(
"https://example.com_asset-library-meta.json"));
}
} // namespace blender::asset_system::tests