Refactor: Remove asset catalog helper classes from public header
These shouldn't need to be accessed from outside the asset system itself, so they should not be exposed with its API. Move them to an own asset system private headers. This is a further step towards making asset system classes more encapuslated, so behavior and data flow can be controlled better (which helps addressing threat safety issues). Personally I've found it quite confusing to work on higher level issues of the asset catalog system, because I got lost in the multiple classes. Hopefully separating them more clearly helps with that too.
This commit is contained in:
@@ -8,7 +8,6 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <set>
|
||||
@@ -17,7 +16,6 @@
|
||||
#include "BLI_function_ref.hh"
|
||||
#include "BLI_map.hh"
|
||||
#include "BLI_set.hh"
|
||||
#include "BLI_string_ref.hh"
|
||||
#include "BLI_uuid.h"
|
||||
#include "BLI_vector.hh"
|
||||
|
||||
@@ -279,120 +277,6 @@ class AssetCatalogService {
|
||||
const OwningAssetCatalogMap &get_deleted_catalogs() const;
|
||||
};
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
public:
|
||||
AssetCatalogCollection() = default;
|
||||
AssetCatalogCollection(const AssetCatalogCollection &other) = delete;
|
||||
AssetCatalogCollection(AssetCatalogCollection &&other) noexcept = default;
|
||||
|
||||
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);
|
||||
};
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
CatalogFilePath file_path;
|
||||
|
||||
public:
|
||||
AssetCatalogDefinitionFile() = default;
|
||||
|
||||
/**
|
||||
* Write the catalog definitions to the same file they were read from.
|
||||
* Return true when the file was written correctly, false when there was a problem.
|
||||
*/
|
||||
bool write_to_disk() const;
|
||||
/**
|
||||
* Write the catalog definitions to an arbitrary file path.
|
||||
*
|
||||
* Any existing file is backed up to "filename~". Any previously existing backup is overwritten.
|
||||
*
|
||||
* Return true when the file was written correctly, false when there was a problem.
|
||||
*/
|
||||
bool write_to_disk(const CatalogFilePath &dest_file_path) const;
|
||||
|
||||
bool contains(CatalogID catalog_id) const;
|
||||
/** Add a 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 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;
|
||||
};
|
||||
|
||||
/**
|
||||
* Asset Catalog definition, containing a symbolic ID and a path that points to a node in the
|
||||
* catalog hierarchy.
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <map>
|
||||
|
||||
#include "AS_asset_catalog.hh"
|
||||
|
||||
namespace blender::asset_system {
|
||||
|
||||
@@ -13,6 +13,8 @@ set(INC_SYS
|
||||
|
||||
set(SRC
|
||||
intern/asset_catalog.cc
|
||||
intern/asset_catalog_collection.cc
|
||||
intern/asset_catalog_definition_file.cc
|
||||
intern/asset_catalog_path.cc
|
||||
intern/asset_catalog_tree.cc
|
||||
intern/asset_identifier.cc
|
||||
@@ -34,6 +36,8 @@ set(SRC
|
||||
AS_asset_library.hh
|
||||
AS_asset_representation.hh
|
||||
AS_essentials_library.hh
|
||||
intern/asset_catalog_collection.hh
|
||||
intern/asset_catalog_definition_file.hh
|
||||
intern/asset_library_all.hh
|
||||
intern/asset_library_essentials.hh
|
||||
intern/asset_library_from_preferences.hh
|
||||
|
||||
@@ -6,15 +6,16 @@
|
||||
* \ingroup asset_system
|
||||
*/
|
||||
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <set>
|
||||
|
||||
#include "AS_asset_catalog.hh"
|
||||
#include "AS_asset_catalog_tree.hh"
|
||||
#include "AS_asset_library.hh"
|
||||
#include "asset_catalog_collection.hh"
|
||||
#include "asset_catalog_definition_file.hh"
|
||||
|
||||
#include "BLI_fileops.hh"
|
||||
#include "BLI_fileops.h"
|
||||
#include "BLI_path_util.h"
|
||||
|
||||
/* For S_ISREG() and S_ISDIR() on Windows. */
|
||||
@@ -32,16 +33,6 @@ namespace blender::asset_system {
|
||||
|
||||
const CatalogFilePath AssetCatalogService::DEFAULT_CATALOG_FILENAME = "blender_assets.cats.txt";
|
||||
|
||||
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";
|
||||
|
||||
AssetCatalogService::AssetCatalogService()
|
||||
: catalog_collection_(std::make_unique<AssetCatalogCollection>())
|
||||
{
|
||||
@@ -689,313 +680,6 @@ void AssetCatalogService::undo_push()
|
||||
|
||||
/* ---------------------------------------------------------------------- */
|
||||
|
||||
std::unique_ptr<AssetCatalogCollection> AssetCatalogCollection::deep_copy() const
|
||||
{
|
||||
auto copy = std::make_unique<AssetCatalogCollection>();
|
||||
|
||||
copy->has_unsaved_changes_ = this->has_unsaved_changes_;
|
||||
copy->catalogs_ = this->copy_catalog_map(this->catalogs_);
|
||||
copy->deleted_catalogs_ = this->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;
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------- */
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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::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(!"A CDF should only reference known catalogs.");
|
||||
}
|
||||
|
||||
return copy;
|
||||
}
|
||||
|
||||
AssetCatalog::AssetCatalog(const CatalogID catalog_id,
|
||||
const AssetCatalogPath &path,
|
||||
const std::string &simple_name)
|
||||
@@ -1030,6 +714,8 @@ std::string AssetCatalog::sensible_simple_name_for_path(const AssetCatalogPath &
|
||||
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)),
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
/* 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 {
|
||||
|
||||
std::unique_ptr<AssetCatalogCollection> AssetCatalogCollection::deep_copy() const
|
||||
{
|
||||
auto copy = std::make_unique<AssetCatalogCollection>();
|
||||
|
||||
copy->has_unsaved_changes_ = this->has_unsaved_changes_;
|
||||
copy->catalogs_ = this->copy_catalog_map(this->catalogs_);
|
||||
copy->deleted_catalogs_ = this->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,61 @@
|
||||
/* 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 {
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
public:
|
||||
AssetCatalogCollection() = default;
|
||||
AssetCatalogCollection(const AssetCatalogCollection &other) = delete;
|
||||
AssetCatalogCollection(AssetCatalogCollection &&other) noexcept = default;
|
||||
|
||||
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);
|
||||
};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
/* 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_util.h"
|
||||
|
||||
#include "CLG_log.h"
|
||||
|
||||
#include "asset_catalog_definition_file.hh"
|
||||
|
||||
static CLG_LogRef LOG = {"asset_system.asset_catalog_definition_file"};
|
||||
|
||||
namespace blender::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);
|
||||
}
|
||||
|
||||
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::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(!"A CDF should only reference known catalogs.");
|
||||
}
|
||||
|
||||
return copy;
|
||||
}
|
||||
|
||||
} // namespace blender::asset_system
|
||||
@@ -0,0 +1,87 @@
|
||||
/* 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;
|
||||
|
||||
CatalogFilePath file_path;
|
||||
|
||||
public:
|
||||
AssetCatalogDefinitionFile() = default;
|
||||
|
||||
/**
|
||||
* Write the catalog definitions to the same file they were read from.
|
||||
* Return true when the file was written correctly, false when there was a problem.
|
||||
*/
|
||||
bool write_to_disk() const;
|
||||
/**
|
||||
* Write the catalog definitions to an arbitrary file path.
|
||||
*
|
||||
* Any existing file is backed up to "filename~". Any previously existing backup is overwritten.
|
||||
*
|
||||
* Return true when the file was written correctly, false when there was a problem.
|
||||
*/
|
||||
bool write_to_disk(const CatalogFilePath &dest_file_path) const;
|
||||
|
||||
bool contains(CatalogID catalog_id) const;
|
||||
/** Add a 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 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
|
||||
@@ -24,6 +24,8 @@
|
||||
|
||||
#include "DNA_userdef_types.h"
|
||||
|
||||
#include "asset_catalog_collection.hh"
|
||||
#include "asset_catalog_definition_file.hh"
|
||||
#include "asset_library_service.hh"
|
||||
#include "asset_storage.hh"
|
||||
#include "utils.hh"
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
#include <memory>
|
||||
|
||||
#include "AS_asset_catalog_tree.hh"
|
||||
#include "asset_catalog_collection.hh"
|
||||
#include "asset_catalog_definition_file.hh"
|
||||
|
||||
#include "asset_library_all.hh"
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
|
||||
#include "AS_asset_catalog.hh"
|
||||
#include "AS_asset_catalog_tree.hh"
|
||||
#include "asset_catalog_collection.hh"
|
||||
#include "asset_catalog_definition_file.hh"
|
||||
|
||||
#include "BKE_preferences.h"
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
|
||||
#include "AS_asset_catalog.hh"
|
||||
#include "AS_asset_catalog_tree.hh"
|
||||
#include "asset_catalog_collection.hh"
|
||||
#include "asset_catalog_definition_file.hh"
|
||||
|
||||
#include "BLI_path_util.h"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user