Add Chromium-only Blender WebEngine parity work
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
55
blender-5.2.0/source/blender/asset_system/intern/utils.cc
Normal file
55
blender-5.2.0/source/blender/asset_system/intern/utils.cc
Normal 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
|
||||
31
blender-5.2.0/source/blender/asset_system/intern/utils.hh
Normal file
31
blender-5.2.0/source/blender/asset_system/intern/utils.hh
Normal 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
|
||||
Reference in New Issue
Block a user