Blender  V3.3
blenkernel/intern/asset_catalog.cc
Go to the documentation of this file.
1 /* SPDX-License-Identifier: GPL-2.0-or-later */
2 
7 #include <fstream>
8 #include <set>
9 
10 #include "BKE_asset_catalog.hh"
11 #include "BKE_asset_library.h"
12 
13 #include "BLI_fileops.hh"
14 #include "BLI_path_util.h"
15 
16 /* For S_ISREG() and S_ISDIR() on Windows. */
17 #ifdef WIN32
18 # include "BLI_winstuff.h"
19 #endif
20 
21 #include "CLG_log.h"
22 
23 static CLG_LogRef LOG = {"bke.asset_service"};
24 
25 namespace blender::bke {
26 
27 const CatalogFilePath AssetCatalogService::DEFAULT_CATALOG_FILENAME = "blender_assets.cats.txt";
28 
30 const std::string AssetCatalogDefinitionFile::VERSION_MARKER = "VERSION ";
31 
32 const std::string AssetCatalogDefinitionFile::HEADER =
33  "# This is an Asset Catalog Definition file for Blender.\n"
34  "#\n"
35  "# Empty lines and lines starting with `#` will be ignored.\n"
36  "# The first non-ignored line should be the version indicator.\n"
37  "# Other lines are of the format \"UUID:catalog/path/for/assets:simple catalog name\"\n";
38 
40  : catalog_collection_(std::make_unique<AssetCatalogCollection>())
41 {
42 }
43 
45  : catalog_collection_(std::make_unique<AssetCatalogCollection>()),
46  asset_library_root_(asset_library_root)
47 {
48 }
49 
51 {
52  if (edited_catalog) {
53  edited_catalog->flags.has_unsaved_changes = true;
54  }
56  catalog_collection_->has_unsaved_changes_ = true;
57 }
58 
60 {
62  catalog_collection_->has_unsaved_changes_ = false;
63 
64  /* TODO(Sybren): refactor; this is more like "post-write cleanup" than "remove a tag" code. */
65 
66  /* Forget about any deleted catalogs. */
67  if (catalog_collection_->catalog_definition_file_) {
68  for (CatalogID catalog_id : catalog_collection_->deleted_catalogs_.keys()) {
69  catalog_collection_->catalog_definition_file_->forget(catalog_id);
70  }
71  }
72  catalog_collection_->deleted_catalogs_.clear();
73 
74  /* Mark all remaining catalogs as "without unsaved changes". */
75  for (auto &catalog_uptr : catalog_collection_->catalogs_.values()) {
76  catalog_uptr->flags.has_unsaved_changes = false;
77  }
78 }
79 
81 {
83  return catalog_collection_->has_unsaved_changes_;
84 }
85 
87 {
88  for (auto &catalog : catalog_collection_->catalogs_.values()) {
89  catalog->flags.has_unsaved_changes = true;
90  }
91  catalog_collection_->has_unsaved_changes_ = true;
92 }
93 
95 {
97  return catalog_collection_->catalogs_.is_empty();
98 }
99 
101 {
102  return catalog_collection_->catalogs_;
103 }
105 {
106  return catalog_collection_->deleted_catalogs_;
107 }
108 
110 {
111  return catalog_collection_->catalog_definition_file_.get();
112 }
113 
115 {
116  const std::unique_ptr<AssetCatalog> *catalog_uptr_ptr =
117  catalog_collection_->catalogs_.lookup_ptr(catalog_id);
118  if (catalog_uptr_ptr == nullptr) {
119  return nullptr;
120  }
121  return catalog_uptr_ptr->get();
122 }
123 
125 {
126  /* Use an AssetCatalogOrderedSet to find the 'best' catalog for this path. This will be the first
127  * one loaded from disk, or if that does not exist the one with the lowest UUID. This ensures
128  * stable, predictable results. */
129  MutableAssetCatalogOrderedSet ordered_catalogs;
130 
131  for (const auto &catalog : catalog_collection_->catalogs_.values()) {
132  if (catalog->path == path) {
133  ordered_catalogs.insert(catalog.get());
134  }
135  }
136 
137  if (ordered_catalogs.empty()) {
138  return nullptr;
139  }
140 
141  MutableAssetCatalogOrderedSet::iterator best_choice_it = ordered_catalogs.begin();
142  return *best_choice_it;
143 }
144 
146 {
148  return catalog_collection_->catalogs_.contains(catalog_id);
149 }
150 
152  const CatalogID active_catalog_id) const
153 {
154  Set<CatalogID> matching_catalog_ids;
155  Set<CatalogID> known_catalog_ids;
156  matching_catalog_ids.add(active_catalog_id);
157 
158  const AssetCatalog *active_catalog = find_catalog(active_catalog_id);
159 
160  /* This cannot just iterate over tree items to get all the required data, because tree items only
161  * represent single UUIDs. It could be used to get the main UUIDs of the children, though, and
162  * then only do an exact match on the path (instead of the more complex `is_contained_in()`
163  * call). Without an extra indexed-by-path acceleration structure, this is still going to require
164  * a linear search, though. */
165  for (const auto &catalog_uptr : catalog_collection_->catalogs_.values()) {
166  if (active_catalog && catalog_uptr->path.is_contained_in(active_catalog->path)) {
167  matching_catalog_ids.add(catalog_uptr->catalog_id);
168  }
169  known_catalog_ids.add(catalog_uptr->catalog_id);
170  }
171 
172  return AssetCatalogFilter(std::move(matching_catalog_ids), std::move(known_catalog_ids));
173 }
174 
176 {
177  std::unique_ptr<AssetCatalog> *catalog_uptr_ptr = catalog_collection_->catalogs_.lookup_ptr(
178  catalog_id);
179  if (catalog_uptr_ptr == nullptr) {
180  /* Catalog cannot be found, which is fine. */
181  return;
182  }
183 
184  /* Mark the catalog as deleted. */
185  AssetCatalog *catalog = catalog_uptr_ptr->get();
186  catalog->flags.is_deleted = true;
187 
188  /* Move ownership from catalog_collection_->catalogs_ to catalog_collection_->deleted_catalogs_.
189  */
190  catalog_collection_->deleted_catalogs_.add(catalog_id, std::move(*catalog_uptr_ptr));
191 
192  /* The catalog can now be removed from the map without freeing the actual AssetCatalog. */
193  catalog_collection_->catalogs_.remove(catalog_id);
194 }
195 
197 {
198  catalog_collection_->catalogs_.remove(catalog_id);
199  catalog_collection_->deleted_catalogs_.remove(catalog_id);
200 
201  /* TODO(@sybren): adjust this when supporting multiple CDFs. */
202  catalog_collection_->catalog_definition_file_->forget(catalog_id);
203 }
204 
206 {
207  /* Build a collection of catalog IDs to delete. */
208  Set<CatalogID> catalogs_to_delete;
209  for (const auto &catalog_uptr : catalog_collection_->catalogs_.values()) {
210  const AssetCatalog *cat = catalog_uptr.get();
211  if (cat->path.is_contained_in(path)) {
212  catalogs_to_delete.add(cat->catalog_id);
213  }
214  }
215 
216  /* Delete the catalogs. */
217  for (const CatalogID cat_id : catalogs_to_delete) {
218  this->delete_catalog_by_id_soft(cat_id);
219  }
220 
221  this->rebuild_tree();
222 }
223 
225 {
226  const AssetCatalog *catalog = find_catalog(catalog_id);
227  BLI_assert_msg(catalog, "trying to prune asset catalogs by the path of a non-existent catalog");
228  if (!catalog) {
229  return;
230  }
231  this->prune_catalogs_by_path(catalog->path);
232 }
233 
235  const AssetCatalogPath &new_catalog_path)
236 {
237  AssetCatalog *renamed_cat = this->find_catalog(catalog_id);
238  const AssetCatalogPath old_cat_path = renamed_cat->path;
239 
240  for (auto &catalog_uptr : catalog_collection_->catalogs_.values()) {
241  AssetCatalog *cat = catalog_uptr.get();
242 
243  const AssetCatalogPath new_path = cat->path.rebase(old_cat_path, new_catalog_path);
244  if (!new_path) {
245  continue;
246  }
247  cat->path = new_path;
248  cat->simple_name_refresh();
249  this->tag_has_unsaved_changes(cat);
250 
251  /* TODO(Sybren): go over all assets that are assigned to this catalog, defined in the current
252  * blend file, and update the catalog simple name stored there. */
253  }
254 
255  this->rebuild_tree();
256 }
257 
259 {
260  std::unique_ptr<AssetCatalog> catalog = AssetCatalog::from_path(catalog_path);
261  catalog->flags.has_unsaved_changes = true;
262 
263  /* So we can std::move(catalog) and still use the non-owning pointer: */
264  AssetCatalog *const catalog_ptr = catalog.get();
265 
266  /* TODO(@sybren): move the `AssetCatalog::from_path()` function to another place, that can reuse
267  * catalogs when a catalog with the given path is already known, and avoid duplicate catalog IDs.
268  */
269  BLI_assert_msg(!catalog_collection_->catalogs_.contains(catalog->catalog_id),
270  "duplicate catalog ID not supported");
271  catalog_collection_->catalogs_.add_new(catalog->catalog_id, std::move(catalog));
272 
273  if (catalog_collection_->catalog_definition_file_) {
274  /* Ensure the new catalog gets written to disk at some point. If there is no CDF in memory yet,
275  * it's enough to have the catalog known to the service as it'll be saved to a new file. */
276  catalog_collection_->catalog_definition_file_->add_new(catalog_ptr);
277  }
278 
279  BLI_assert_msg(catalog_tree_, "An Asset Catalog tree should always exist.");
280  catalog_tree_->insert_item(*catalog_ptr);
281 
282  return catalog_ptr;
283 }
284 
285 static std::string asset_definition_default_file_path_from_dir(StringRef asset_library_root)
286 {
287  char file_path[PATH_MAX];
288  BLI_join_dirfile(file_path,
289  sizeof(file_path),
290  asset_library_root.data(),
292  return file_path;
293 }
294 
296 {
298 }
299 
300 void AssetCatalogService::load_from_disk(const CatalogFilePath &file_or_directory_path)
301 {
302  BLI_stat_t status;
303  if (BLI_stat(file_or_directory_path.data(), &status) == -1) {
304  /* TODO(@sybren): throw an appropriate exception. */
305  CLOG_WARN(&LOG, "path not found: %s", file_or_directory_path.data());
306  return;
307  }
308 
309  if (S_ISREG(status.st_mode)) {
310  load_single_file(file_or_directory_path);
311  }
312  else if (S_ISDIR(status.st_mode)) {
313  load_directory_recursive(file_or_directory_path);
314  }
315  else {
316  /* TODO(@sybren): throw an appropriate exception. */
317  }
318 
319  /* TODO: Should there be a sanitize step? E.g. to remove catalogs with identical paths? */
320 
321  rebuild_tree();
322 }
323 
325 {
326  /* TODO(@sybren): implement proper multi-file support. For now, just load
327  * the default file if it is there. */
329 
330  if (!BLI_exists(file_path.data())) {
331  /* No file to be loaded is perfectly fine. */
332  CLOG_INFO(&LOG, 2, "path not found: %s", file_path.data());
333  return;
334  }
335 
336  this->load_single_file(file_path);
337 }
338 
339 void AssetCatalogService::load_single_file(const CatalogFilePath &catalog_definition_file_path)
340 {
341  /* TODO(@sybren): check that #catalog_definition_file_path is contained in #asset_library_root_,
342  * otherwise some assumptions may fail. */
343  std::unique_ptr<AssetCatalogDefinitionFile> cdf = parse_catalog_file(
344  catalog_definition_file_path);
345 
346  BLI_assert_msg(!catalog_collection_->catalog_definition_file_,
347  "Only loading of a single catalog definition file is supported.");
348  catalog_collection_->catalog_definition_file_ = std::move(cdf);
349 }
350 
351 std::unique_ptr<AssetCatalogDefinitionFile> AssetCatalogService::parse_catalog_file(
352  const CatalogFilePath &catalog_definition_file_path)
353 {
354  auto cdf = std::make_unique<AssetCatalogDefinitionFile>();
355  cdf->file_path = catalog_definition_file_path;
356 
357  /* TODO(Sybren): this might have to move to a higher level when supporting multiple CDFs. */
358  Set<AssetCatalogPath> seen_paths;
359 
360  auto catalog_parsed_callback = [this, catalog_definition_file_path, &seen_paths](
361  std::unique_ptr<AssetCatalog> catalog) {
362  if (catalog_collection_->catalogs_.contains(catalog->catalog_id)) {
363  /* TODO(@sybren): apparently another CDF was already loaded. This is not supported yet. */
364  std::cerr << catalog_definition_file_path << ": multiple definitions of catalog "
365  << catalog->catalog_id << " in multiple files, ignoring this one." << std::endl;
366  /* Don't store 'catalog'; unique_ptr will free its memory. */
367  return false;
368  }
369 
370  catalog->flags.is_first_loaded = seen_paths.add(catalog->path);
371 
372  /* The AssetCatalog pointer is now owned by the AssetCatalogService. */
373  catalog_collection_->catalogs_.add_new(catalog->catalog_id, std::move(catalog));
374  return true;
375  };
376 
377  cdf->parse_catalog_file(cdf->file_path, catalog_parsed_callback);
378 
379  return cdf;
380 }
381 
383 {
384  /* TODO(Sybren): expand to support multiple CDFs. */
385  AssetCatalogDefinitionFile *const cdf = catalog_collection_->catalog_definition_file_.get();
386  if (!cdf || cdf->file_path.empty() || !BLI_is_file(cdf->file_path.c_str())) {
387  return;
388  }
389 
390  /* Keeps track of the catalog IDs that are seen in the CDF, so that we also know what was deleted
391  * from the file on disk. */
392  Set<CatalogID> cats_in_file;
393 
394  auto catalog_parsed_callback = [this, &cats_in_file](std::unique_ptr<AssetCatalog> catalog) {
395  const CatalogID catalog_id = catalog->catalog_id;
396  cats_in_file.add(catalog_id);
397 
398  const bool should_skip = is_catalog_known_with_unsaved_changes(catalog_id);
399  if (should_skip) {
400  /* Do not overwrite unsaved local changes. */
401  return false;
402  }
403 
404  /* This is either a new catalog, or we can just replace the in-memory one with the newly loaded
405  * one. */
406  catalog_collection_->catalogs_.add_overwrite(catalog_id, std::move(catalog));
407  return true;
408  };
409 
410  cdf->parse_catalog_file(cdf->file_path, catalog_parsed_callback);
411  this->purge_catalogs_not_listed(cats_in_file);
412  this->rebuild_tree();
413 }
414 
416 {
417  Set<CatalogID> cats_to_remove;
418  for (CatalogID cat_id : this->catalog_collection_->catalogs_.keys()) {
419  if (catalogs_to_keep.contains(cat_id)) {
420  continue;
421  }
423  continue;
424  }
425  /* This catalog is not on disk, but also not modified, so get rid of it. */
426  cats_to_remove.add(cat_id);
427  }
428 
429  for (CatalogID cat_id : cats_to_remove) {
431  }
432 }
433 
435 {
436  if (catalog_collection_->deleted_catalogs_.contains(catalog_id)) {
437  /* Deleted catalogs are always considered modified, by definition. */
438  return true;
439  }
440 
441  const std::unique_ptr<AssetCatalog> *catalog_uptr_ptr =
442  catalog_collection_->catalogs_.lookup_ptr(catalog_id);
443  if (!catalog_uptr_ptr) {
444  /* Catalog is unknown. */
445  return false;
446  }
447 
448  const bool has_unsaved_changes = (*catalog_uptr_ptr)->flags.has_unsaved_changes;
449  return has_unsaved_changes;
450 }
451 
453 {
454  if (!write_to_disk_ex(blend_file_path)) {
455  return false;
456  }
457 
459  rebuild_tree();
460  return true;
461 }
462 
464 {
465  /* TODO(Sybren): expand to support multiple CDFs. */
466 
467  /* - Already loaded a CDF from disk? -> Always write to that file. */
468  if (catalog_collection_->catalog_definition_file_) {
469  reload_catalogs();
470  return catalog_collection_->catalog_definition_file_->write_to_disk();
471  }
472 
473  if (catalog_collection_->catalogs_.is_empty() &&
474  catalog_collection_->deleted_catalogs_.is_empty()) {
475  /* Avoid saving anything, when there is nothing to save. */
476  return true; /* Writing nothing when there is nothing to write is still a success. */
477  }
478 
479  const CatalogFilePath cdf_path_to_write = find_suitable_cdf_path_for_writing(blend_file_path);
480  catalog_collection_->catalog_definition_file_ = construct_cdf_in_memory(cdf_path_to_write);
481  reload_catalogs();
482  return catalog_collection_->catalog_definition_file_->write_to_disk();
483 }
484 
486 {
487  /* TODO(Sybren): expand to support multiple CDFs. */
488 
489  if (!catalog_collection_->catalog_definition_file_) {
490  /* There is no CDF connected, so it's a no-op. */
491  return;
492  }
493 
494  /* Remove any association with the CDF, so that a new location will be chosen
495  * when the blend file is saved. */
496  catalog_collection_->catalog_definition_file_.reset();
497 
498  /* Mark all in-memory catalogs as "dirty", to force them to be kept around on
499  * the next "load-merge-write" cycle. */
501 }
502 
504  const CatalogFilePath &blend_file_path)
505 {
506  BLI_assert_msg(!blend_file_path.empty(),
507  "A non-empty .blend file path is required to be able to determine where the "
508  "catalog definition file should be put");
509 
510  /* Ask the asset library API for an appropriate location. */
511  char suitable_root_path[PATH_MAX];
512  const bool asset_lib_root_found = BKE_asset_library_find_suitable_root_path_from_path(
513  blend_file_path.c_str(), suitable_root_path);
514  if (asset_lib_root_found) {
515  char asset_lib_cdf_path[PATH_MAX];
516  BLI_path_join(asset_lib_cdf_path,
517  sizeof(asset_lib_cdf_path),
518  suitable_root_path,
519  DEFAULT_CATALOG_FILENAME.c_str(),
520  NULL);
521  return asset_lib_cdf_path;
522  }
523 
524  /* Determine the default CDF path in the same directory of the blend file. */
525  char blend_dir_path[PATH_MAX];
526  BLI_split_dir_part(blend_file_path.c_str(), blend_dir_path, sizeof(blend_dir_path));
527  const CatalogFilePath cdf_path_next_to_blend = asset_definition_default_file_path_from_dir(
528  blend_dir_path);
529  return cdf_path_next_to_blend;
530 }
531 
532 std::unique_ptr<AssetCatalogDefinitionFile> AssetCatalogService::construct_cdf_in_memory(
533  const CatalogFilePath &file_path)
534 {
535  auto cdf = std::make_unique<AssetCatalogDefinitionFile>();
536  cdf->file_path = file_path;
537 
538  for (auto &catalog : catalog_collection_->catalogs_.values()) {
539  cdf->add_new(catalog.get());
540  }
541 
542  return cdf;
543 }
544 
546 {
547  return catalog_tree_.get();
548 }
549 
550 std::unique_ptr<AssetCatalogTree> AssetCatalogService::read_into_tree()
551 {
552  auto tree = std::make_unique<AssetCatalogTree>();
553 
554  /* Go through the catalogs, insert each path component into the tree where needed. */
555  for (auto &catalog : catalog_collection_->catalogs_.values()) {
556  tree->insert_item(*catalog);
557  }
558 
559  return tree;
560 }
561 
563 {
565  this->catalog_tree_ = read_into_tree();
566 }
567 
569 {
570  /* Construct an ordered set of paths to check, so that parents are ordered before children. */
571  std::set<AssetCatalogPath> paths_to_check;
572  for (auto &catalog : catalog_collection_->catalogs_.values()) {
573  paths_to_check.insert(catalog->path);
574  }
575 
576  std::set<AssetCatalogPath> seen_paths;
577  /* The empty parent should never be created, so always be considered "seen". */
578  seen_paths.insert(AssetCatalogPath(""));
579 
580  /* Find and create missing direct parents (so ignoring parents-of-parents). */
581  while (!paths_to_check.empty()) {
582  /* Pop the first path of the queue. */
583  const AssetCatalogPath path = *paths_to_check.begin();
584  paths_to_check.erase(paths_to_check.begin());
585 
586  if (seen_paths.find(path) != seen_paths.end()) {
587  /* This path has been seen already, so it can be ignored. */
588  continue;
589  }
590  seen_paths.insert(path);
591 
592  const AssetCatalogPath parent_path = path.parent();
593  if (seen_paths.find(parent_path) != seen_paths.end()) {
594  /* The parent exists, continue to the next path. */
595  continue;
596  }
597 
598  /* The parent doesn't exist, so create it and queue it up for checking its parent. */
599  AssetCatalog *parent_catalog = create_catalog(parent_path);
600  parent_catalog->flags.has_unsaved_changes = true;
601 
602  paths_to_check.insert(parent_path);
603  }
604 
605  /* TODO(Sybren): bind the newly created catalogs to a CDF, if we know about it. */
606 }
607 
609 {
610  return !undo_snapshots_.is_empty();
611 }
612 
614 {
615  return !redo_snapshots_.is_empty();
616 }
617 
619 {
620  BLI_assert_msg(is_undo_possbile(), "Undo stack is empty");
621 
622  redo_snapshots_.append(std::move(catalog_collection_));
624  rebuild_tree();
625 }
626 
628 {
629  BLI_assert_msg(is_redo_possbile(), "Redo stack is empty");
630 
631  undo_snapshots_.append(std::move(catalog_collection_));
633  rebuild_tree();
634 }
635 
637 {
638  std::unique_ptr<AssetCatalogCollection> snapshot = catalog_collection_->deep_copy();
639  undo_snapshots_.append(std::move(snapshot));
640  redo_snapshots_.clear();
641 }
642 
643 /* ---------------------------------------------------------------------- */
644 
645 std::unique_ptr<AssetCatalogCollection> AssetCatalogCollection::deep_copy() const
646 {
647  auto copy = std::make_unique<AssetCatalogCollection>();
648 
649  copy->has_unsaved_changes_ = this->has_unsaved_changes_;
650  copy->catalogs_ = copy_catalog_map(this->catalogs_);
651  copy->deleted_catalogs_ = copy_catalog_map(this->deleted_catalogs_);
652 
654  copy->catalog_definition_file_ = catalog_definition_file_->copy_and_remap(
655  copy->catalogs_, copy->deleted_catalogs_);
656  }
657 
658  return copy;
659 }
660 
662 {
664 
665  for (const auto &orig_catalog_uptr : orig.values()) {
666  auto copy_catalog_uptr = std::make_unique<AssetCatalog>(*orig_catalog_uptr);
667  copy.add_new(copy_catalog_uptr->catalog_id, std::move(copy_catalog_uptr));
668  }
669 
670  return copy;
671 }
672 
673 /* ---------------------------------------------------------------------- */
674 
676  CatalogID catalog_id,
677  StringRef simple_name,
678  const AssetCatalogTreeItem *parent)
679  : name_(name), catalog_id_(catalog_id), simple_name_(simple_name), parent_(parent)
680 {
681 }
682 
684 {
685  return catalog_id_;
686 }
687 
689 {
690  return name_;
691 }
692 
694 {
695  return simple_name_;
696 }
698 {
699  return has_unsaved_changes_;
700 }
701 
703 {
704  AssetCatalogPath current_path = name_;
705  for (const AssetCatalogTreeItem *parent = parent_; parent; parent = parent->parent_) {
706  current_path = AssetCatalogPath(parent->name_) / current_path;
707  }
708  return current_path;
709 }
710 
712 {
713  int i = 0;
714  for (const AssetCatalogTreeItem *parent = parent_; parent; parent = parent->parent_) {
715  i++;
716  }
717  return i;
718 }
719 
721 {
722  return !children_.empty();
723 }
724 
725 void AssetCatalogTreeItem::foreach_item_recursive(AssetCatalogTreeItem::ChildMap &children,
726  const ItemIterFn callback)
727 {
728  for (auto &[key, item] : children) {
729  callback(item);
730  foreach_item_recursive(item.children_, callback);
731  }
732 }
733 
735 {
736  for (auto &[key, item] : children_) {
737  callback(item);
738  }
739 }
740 
741 /* ---------------------------------------------------------------------- */
742 
744 {
745  const AssetCatalogTreeItem *parent = nullptr;
746  /* The children for the currently iterated component, where the following component should be
747  * added to (if not there yet). */
748  AssetCatalogTreeItem::ChildMap *current_item_children = &root_items_;
749 
750  BLI_assert_msg(!ELEM(catalog.path.str()[0], '/', '\\'),
751  "Malformed catalog path; should not start with a separator");
752 
753  const CatalogID nil_id{};
754 
755  catalog.path.iterate_components([&](StringRef component_name, const bool is_last_component) {
756  /* Insert new tree element - if no matching one is there yet! */
757  auto [key_and_item, was_inserted] = current_item_children->emplace(
758  component_name,
759  AssetCatalogTreeItem(component_name,
760  is_last_component ? catalog.catalog_id : nil_id,
761  is_last_component ? catalog.simple_name : "",
762  parent));
763  AssetCatalogTreeItem &item = key_and_item->second;
764 
765  /* If full path of this catalog already exists as parent path of a previously read catalog,
766  * we can ensure this tree item's UUID is set here. */
767  if (is_last_component) {
768  if (BLI_uuid_is_nil(item.catalog_id_) || catalog.flags.is_first_loaded) {
769  item.catalog_id_ = catalog.catalog_id;
770  }
771  item.has_unsaved_changes_ = catalog.flags.has_unsaved_changes;
772  }
773 
774  /* Walk further into the path (no matter if a new item was created or not). */
775  parent = &item;
776  current_item_children = &item.children_;
777  });
778 }
779 
781 {
782  AssetCatalogTreeItem::foreach_item_recursive(root_items_, callback);
783 }
784 
786 {
787  for (auto &[key, item] : root_items_) {
788  callback(item);
789  }
790 }
791 
792 /* ---------------------------------------------------------------------- */
793 
794 /* ---------------------------------------------------------------------- */
795 
797 {
798  return catalogs_.contains(catalog_id);
799 }
800 
802 {
803  catalogs_.add_new(catalog->catalog_id, catalog);
804 }
805 
807 {
808  catalogs_.add_overwrite(catalog->catalog_id, catalog);
809 }
810 
812 {
813  catalogs_.remove(catalog_id);
814 }
815 
817  const CatalogFilePath &catalog_definition_file_path,
818  AssetCatalogParsedFn catalog_loaded_callback)
819 {
820  fstream infile(catalog_definition_file_path, std::ios::in);
821 
822  if (!infile.is_open()) {
823  CLOG_ERROR(&LOG, "%s: unable to open file", catalog_definition_file_path.c_str());
824  return;
825  }
826  bool seen_version_number = false;
827  std::string line;
828  while (std::getline(infile, line)) {
829  const StringRef trimmed_line = StringRef(line).trim();
830  if (trimmed_line.is_empty() || trimmed_line[0] == '#') {
831  continue;
832  }
833 
834  if (!seen_version_number) {
835  /* The very first non-ignored line should be the version declaration. */
836  const bool is_valid_version = this->parse_version_line(trimmed_line);
837  if (!is_valid_version) {
838  std::cerr << catalog_definition_file_path
839  << ": first line should be version declaration; ignoring file." << std::endl;
840  break;
841  }
842  seen_version_number = true;
843  continue;
844  }
845 
846  std::unique_ptr<AssetCatalog> catalog = this->parse_catalog_line(trimmed_line);
847  if (!catalog) {
848  continue;
849  }
850 
851  AssetCatalog *non_owning_ptr = catalog.get();
852  const bool keep_catalog = catalog_loaded_callback(std::move(catalog));
853  if (!keep_catalog) {
854  continue;
855  }
856 
857  /* The AssetDefinitionFile should include this catalog when writing it back to disk. */
858  this->add_overwrite(non_owning_ptr);
859  }
860 }
861 
863 {
864  if (!line.startswith(VERSION_MARKER)) {
865  return false;
866  }
867 
868  const std::string version_string = line.substr(VERSION_MARKER.length());
869  const int file_version = std::atoi(version_string.c_str());
870 
871  /* No versioning, just a blunt check whether it's the right one. */
872  return file_version == SUPPORTED_VERSION;
873 }
874 
875 std::unique_ptr<AssetCatalog> AssetCatalogDefinitionFile::parse_catalog_line(const StringRef line)
876 {
877  const char delim = ':';
878  const int64_t first_delim = line.find_first_of(delim);
879  if (first_delim == StringRef::not_found) {
880  std::cerr << "Invalid catalog line in " << this->file_path << ": " << line << std::endl;
881  return std::unique_ptr<AssetCatalog>(nullptr);
882  }
883 
884  /* Parse the catalog ID. */
885  const std::string id_as_string = line.substr(0, first_delim).trim();
886  bUUID catalog_id;
887  const bool uuid_parsed_ok = BLI_uuid_parse_string(&catalog_id, id_as_string.c_str());
888  if (!uuid_parsed_ok) {
889  std::cerr << "Invalid UUID in " << this->file_path << ": " << line << std::endl;
890  return std::unique_ptr<AssetCatalog>(nullptr);
891  }
892 
893  /* Parse the path and simple name. */
894  const StringRef path_and_simple_name = line.substr(first_delim + 1);
895  const int64_t second_delim = path_and_simple_name.find_first_of(delim);
896 
897  std::string path_in_file;
898  std::string simple_name;
899  if (second_delim == 0) {
900  /* Delimiter as first character means there is no path. These lines are to be ignored. */
901  return std::unique_ptr<AssetCatalog>(nullptr);
902  }
903 
904  if (second_delim == StringRef::not_found) {
905  /* No delimiter means no simple name, just treat it as all "path". */
906  path_in_file = path_and_simple_name;
907  simple_name = "";
908  }
909  else {
910  path_in_file = path_and_simple_name.substr(0, second_delim);
911  simple_name = path_and_simple_name.substr(second_delim + 1).trim();
912  }
913 
914  AssetCatalogPath catalog_path = path_in_file;
915  return std::make_unique<AssetCatalog>(catalog_id, catalog_path.cleanup(), simple_name);
916 }
917 
919 {
920  BLI_assert_msg(!this->file_path.empty(), "Writing to CDF requires its file path to be known");
921  return this->write_to_disk(this->file_path);
922 }
923 
925 {
926  const CatalogFilePath writable_path = dest_file_path + ".writing";
927  const CatalogFilePath backup_path = dest_file_path + "~";
928 
929  if (!this->write_to_disk_unsafe(writable_path)) {
930  /* TODO: communicate what went wrong. */
931  return false;
932  }
933  if (BLI_exists(dest_file_path.c_str())) {
934  if (BLI_rename(dest_file_path.c_str(), backup_path.c_str())) {
935  /* TODO: communicate what went wrong. */
936  return false;
937  }
938  }
939  if (BLI_rename(writable_path.c_str(), dest_file_path.c_str())) {
940  /* TODO: communicate what went wrong. */
941  return false;
942  }
943 
944  return true;
945 }
946 
948 {
949  char directory[PATH_MAX];
950  BLI_split_dir_part(dest_file_path.c_str(), directory, sizeof(directory));
951  if (!ensure_directory_exists(directory)) {
952  /* TODO(Sybren): pass errors to the UI somehow. */
953  return false;
954  }
955 
956  fstream output(dest_file_path, std::ios::out);
957 
958  /* TODO(@sybren): remember the line ending style that was originally read, then use that to write
959  * the file again. */
960 
961  /* Write the header. */
962  output << HEADER;
963  output << "" << std::endl;
964  output << VERSION_MARKER << SUPPORTED_VERSION << std::endl;
965  output << "" << std::endl;
966 
967  /* Write the catalogs, ordered by path (primary) and UUID (secondary). */
968  AssetCatalogOrderedSet catalogs_by_path;
969  for (const AssetCatalog *catalog : catalogs_.values()) {
970  if (catalog->flags.is_deleted) {
971  continue;
972  }
973  catalogs_by_path.insert(catalog);
974  }
975 
976  for (const AssetCatalog *catalog : catalogs_by_path) {
977  output << catalog->catalog_id << ":" << catalog->path << ":" << catalog->simple_name
978  << std::endl;
979  }
980  output.close();
981  return !output.bad();
982 }
983 
985  const CatalogFilePath directory_path) const
986 {
987  /* TODO(@sybren): design a way to get such errors presented to users (or ensure that they never
988  * occur). */
989  if (directory_path.empty()) {
990  std::cerr
991  << "AssetCatalogService: no asset library root configured, unable to ensure it exists."
992  << std::endl;
993  return false;
994  }
995 
996  if (BLI_exists(directory_path.data())) {
997  if (!BLI_is_dir(directory_path.data())) {
998  std::cerr << "AssetCatalogService: " << directory_path
999  << " exists but is not a directory, this is not a supported situation."
1000  << std::endl;
1001  return false;
1002  }
1003 
1004  /* Root directory exists, work is done. */
1005  return true;
1006  }
1007 
1008  /* Ensure the root directory exists. */
1009  std::error_code err_code;
1010  if (!BLI_dir_create_recursive(directory_path.data())) {
1011  std::cerr << "AssetCatalogService: error creating directory " << directory_path << ": "
1012  << err_code << std::endl;
1013  return false;
1014  }
1015 
1016  /* Root directory has been created, work is done. */
1017  return true;
1018 }
1019 
1020 std::unique_ptr<AssetCatalogDefinitionFile> AssetCatalogDefinitionFile::copy_and_remap(
1021  const OwningAssetCatalogMap &catalogs, const OwningAssetCatalogMap &deleted_catalogs) const
1022 {
1023  auto copy = std::make_unique<AssetCatalogDefinitionFile>(*this);
1024  copy->catalogs_.clear();
1025 
1026  /* Remap pointers of the copy from the original AssetCatalogCollection to the given one. */
1027  for (CatalogID catalog_id : catalogs_.keys()) {
1028  /* The catalog can be in the regular or the deleted map. */
1029  const std::unique_ptr<AssetCatalog> *remapped_catalog_uptr_ptr = catalogs.lookup_ptr(
1030  catalog_id);
1031  if (remapped_catalog_uptr_ptr) {
1032  copy->catalogs_.add_new(catalog_id, remapped_catalog_uptr_ptr->get());
1033  continue;
1034  }
1035 
1036  remapped_catalog_uptr_ptr = deleted_catalogs.lookup_ptr(catalog_id);
1037  if (remapped_catalog_uptr_ptr) {
1038  copy->catalogs_.add_new(catalog_id, remapped_catalog_uptr_ptr->get());
1039  continue;
1040  }
1041 
1042  BLI_assert(!"A CDF should only reference known catalogs.");
1043  }
1044 
1045  return copy;
1046 }
1047 
1049  const AssetCatalogPath &path,
1050  const std::string &simple_name)
1051  : catalog_id(catalog_id), path(path), simple_name(simple_name)
1052 {
1053 }
1054 
1055 std::unique_ptr<AssetCatalog> AssetCatalog::from_path(const AssetCatalogPath &path)
1056 {
1057  const AssetCatalogPath clean_path = path.cleanup();
1058  const CatalogID cat_id = BLI_uuid_generate_random();
1059  const std::string simple_name = sensible_simple_name_for_path(clean_path);
1060  auto catalog = std::make_unique<AssetCatalog>(cat_id, clean_path, simple_name);
1061  return catalog;
1062 }
1063 
1065 {
1067 }
1068 
1070 {
1071  std::string name = path.str();
1072  std::replace(name.begin(), name.end(), AssetCatalogPath::SEPARATOR, '-');
1073  if (name.length() < MAX_NAME - 1) {
1074  return name;
1075  }
1076 
1077  /* Trim off the start of the path, as that's the most generic part and thus contains the least
1078  * information. */
1079  return "..." + name.substr(name.length() - 60);
1080 }
1081 
1083  Set<CatalogID> &&known_catalog_ids)
1084  : matching_catalog_ids(std::move(matching_catalog_ids)),
1085  known_catalog_ids(std::move(known_catalog_ids))
1086 {
1087 }
1088 
1089 bool AssetCatalogFilter::contains(const CatalogID asset_catalog_id) const
1090 {
1091  return matching_catalog_ids.contains(asset_catalog_id);
1092 }
1093 
1094 bool AssetCatalogFilter::is_known(const CatalogID asset_catalog_id) const
1095 {
1096  if (BLI_uuid_is_nil(asset_catalog_id)) {
1097  return false;
1098  }
1099  return known_catalog_ids.contains(asset_catalog_id);
1100 }
1101 
1102 } // namespace blender::bke
bool BKE_asset_library_find_suitable_root_path_from_path(const char *input_path, char r_library_path[768])
#define BLI_assert(a)
Definition: BLI_assert.h:46
#define BLI_assert_msg(a, msg)
Definition: BLI_assert.h:53
int BLI_exists(const char *path) ATTR_WARN_UNUSED_RESULT ATTR_NONNULL()
Definition: storage.c:314
int BLI_stat(const char *path, BLI_stat_t *buffer) ATTR_WARN_UNUSED_RESULT ATTR_NONNULL()
bool BLI_is_file(const char *path) ATTR_WARN_UNUSED_RESULT ATTR_NONNULL()
Definition: storage.c:402
int BLI_rename(const char *from, const char *to) ATTR_NONNULL()
Definition: fileops.c:1268
struct stat BLI_stat_t
Definition: BLI_fileops.h:73
bool BLI_is_dir(const char *path) ATTR_WARN_UNUSED_RESULT ATTR_NONNULL()
Definition: storage.c:397
bool BLI_dir_create_recursive(const char *dir) ATTR_NONNULL()
Definition: fileops.c:1219
#define PATH_MAX
Definition: BLI_fileops.h:29
File and directory operations.
void BLI_split_dir_part(const char *string, char *dir, size_t dirlen)
Definition: path_util.c:1490
size_t BLI_path_join(char *__restrict dst, size_t dst_len, const char *path_first,...) ATTR_NONNULL(1
void BLI_join_dirfile(char *__restrict dst, size_t maxlen, const char *__restrict dir, const char *__restrict file) ATTR_NONNULL()
Definition: path_util.c:1531
#define ELEM(...)
bool BLI_uuid_parse_string(bUUID *uuid, const char *buffer) ATTR_NONNULL()
Definition: uuid.cc:103
bool BLI_uuid_is_nil(bUUID uuid)
Definition: uuid.cc:76
bUUID BLI_uuid_generate_random(void)
Definition: uuid.cc:21
Compatibility-like things for windows.
#define S_ISDIR(x)
Definition: BLI_winstuff.h:48
#define S_ISREG(x)
Definition: BLI_winstuff.h:45
#define CLOG_ERROR(clg_ref,...)
Definition: CLG_log.h:190
#define CLOG_WARN(clg_ref,...)
Definition: CLG_log.h:189
#define CLOG_INFO(clg_ref, level,...)
Definition: CLG_log.h:187
#define MAX_NAME
Definition: DNA_defs.h:48
static CLG_LogRef LOG
ValueIterator values() const
Definition: BLI_map.hh:840
const Value * lookup_ptr(const Key &key) const
Definition: BLI_map.hh:463
bool contains(const Key &key) const
Definition: BLI_set.hh:296
bool add(const Key &key)
Definition: BLI_set.hh:253
static constexpr int64_t not_found
constexpr bool is_empty() const
constexpr StringRef substr(int64_t start, int64_t size) const
constexpr bool startswith(StringRef prefix) const
constexpr int64_t find_first_of(StringRef chars, int64_t pos=0) const
constexpr StringRef trim() const
constexpr const char * data() const
static OwningAssetCatalogMap copy_catalog_map(const OwningAssetCatalogMap &orig)
std::unique_ptr< AssetCatalogCollection > deep_copy() const
std::unique_ptr< AssetCatalogDefinitionFile > catalog_definition_file_
void parse_catalog_file(const CatalogFilePath &catalog_definition_file_path, AssetCatalogParsedFn callback)
bool ensure_directory_exists(const CatalogFilePath directory_path) const
bool write_to_disk_unsafe(const CatalogFilePath &dest_file_path) const
Map< CatalogID, AssetCatalog * > catalogs_
std::unique_ptr< AssetCatalog > parse_catalog_line(StringRef line)
std::unique_ptr< AssetCatalogDefinitionFile > copy_and_remap(const OwningAssetCatalogMap &catalogs, const OwningAssetCatalogMap &deleted_catalogs) const
const Set< CatalogID > matching_catalog_ids
bool is_known(CatalogID asset_catalog_id) const
const Set< CatalogID > known_catalog_ids
bool contains(CatalogID asset_catalog_id) const
AssetCatalogFilter(Set< CatalogID > &&matching_catalog_ids, Set< CatalogID > &&known_catalog_ids)
const std::string & str() const
AssetCatalogPath cleanup() const
bool is_contained_in(const AssetCatalogPath &other_path) const
void iterate_components(ComponentIteratorFn callback) const
AssetCatalogPath rebase(const AssetCatalogPath &from_path, const AssetCatalogPath &to_path) const
AssetCatalogPath parent() const
std::unique_ptr< AssetCatalogDefinitionFile > parse_catalog_file(const CatalogFilePath &catalog_definition_file_path)
void load_directory_recursive(const CatalogFilePath &directory_path)
std::unique_ptr< AssetCatalogTree > catalog_tree_
AssetCatalog * create_catalog(const AssetCatalogPath &catalog_path)
bool write_to_disk(const CatalogFilePath &blend_file_path)
AssetCatalog * find_catalog_by_path(const AssetCatalogPath &path) const
void update_catalog_path(CatalogID catalog_id, const AssetCatalogPath &new_catalog_path)
void tag_has_unsaved_changes(AssetCatalog *edited_catalog)
AssetCatalogDefinitionFile * get_catalog_definition_file()
AssetCatalogFilter create_catalog_filter(CatalogID active_catalog_id) const
std::unique_ptr< AssetCatalogDefinitionFile > construct_cdf_in_memory(const CatalogFilePath &file_path)
AssetCatalog * find_catalog(CatalogID catalog_id) const
static CatalogFilePath find_suitable_cdf_path_for_writing(const CatalogFilePath &blend_file_path)
std::unique_ptr< AssetCatalogTree > read_into_tree()
Vector< std::unique_ptr< AssetCatalogCollection > > undo_snapshots_
void load_single_file(const CatalogFilePath &catalog_definition_file_path)
bool is_catalog_known_with_unsaved_changes(CatalogID catalog_id) const
std::unique_ptr< AssetCatalogCollection > catalog_collection_
void prune_catalogs_by_path(const AssetCatalogPath &path)
void purge_catalogs_not_listed(const Set< CatalogID > &catalogs_to_keep)
static const CatalogFilePath DEFAULT_CATALOG_FILENAME
Vector< std::unique_ptr< AssetCatalogCollection > > redo_snapshots_
bool is_catalog_known(CatalogID catalog_id) const
bool write_to_disk_ex(const CatalogFilePath &blend_file_path)
AssetCatalogTreeItem(StringRef name, CatalogID catalog_id, StringRef simple_name, const AssetCatalogTreeItem *parent=nullptr)
std::map< std::string, AssetCatalogTreeItem > ChildMap
const AssetCatalogTreeItem * parent_
void insert_item(const AssetCatalog &catalog)
void foreach_item(const AssetCatalogTreeItem::ItemIterFn callback)
void foreach_root_item(const ItemIterFn callback)
struct blender::bke::AssetCatalog::Flags flags
static std::unique_ptr< AssetCatalog > from_path(const AssetCatalogPath &path)
static std::string sensible_simple_name_for_path(const AssetCatalogPath &path)
DEGForeachIDComponentCallback callback
void * tree
ccl_global KernelShaderEvalInput ccl_global float * output
std::set< AssetCatalog *, AssetCatalogLessThan > MutableAssetCatalogOrderedSet
std::string CatalogFilePath
std::set< const AssetCatalog *, AssetCatalogLessThan > AssetCatalogOrderedSet
static std::string asset_definition_default_file_path_from_dir(StringRef asset_library_root)
static const pxr::TfToken out("out", pxr::TfToken::Immortal)
static void copy(bNodeTree *dest_ntree, bNode *dest_node, const bNode *src_node)
__int64 int64_t
Definition: stdint.h:89
Universally Unique Identifier according to RFC4122.