diff --git a/ops/add_categories_to_library.py b/ops/add_categories_to_library.py index 95ea31e..c25339f 100644 --- a/ops/add_categories_to_library.py +++ b/ops/add_categories_to_library.py @@ -47,7 +47,6 @@ def execute(self, context: bpy.types.Context) -> set[str]: hive_mind.load_categories() with lib.open_catalogs_file() as cat_file: - cat_file: utils.CatalogsFile for category_uuid, sub_list in hive_mind.SUBCATEGORIES_DICT.items(): cat_info = hive_mind.CATEGORIES_DICT.get(category_uuid) cat = cat_file.find_catalog(category_uuid) diff --git a/ops/add_to_library.py b/ops/add_to_library.py index 2177c8e..f6fe04c 100644 --- a/ops/add_to_library.py +++ b/ops/add_to_library.py @@ -137,15 +137,17 @@ def execute(self, context): / self.new_library_name.replace(" ", "_").casefold() ) dir.mkdir(parents=True, exist_ok=True) - lib = utils.AssetLibrary.create_new_library( + self.lib = utils.AssetLibrary.create_new_library( self.new_library_name, str(dir), context=context, load_catalogs=True ) self.is_new_library = True else: - lib = utils.from_name(self.library, context=context, load_catalogs=True) + self.lib = utils.from_name( + self.library, context=context, load_catalogs=True, load_assets=True + ) self.is_new_library = False - lib.path.mkdir(parents=True, exist_ok=True) + self.lib.path.mkdir(parents=True, exist_ok=True) assets = context.selected_assets @@ -155,11 +157,13 @@ def execute(self, context): if self.keep_blend_files_as_is: self._thread = Thread( - target=self.add_to_library_keep, args=(assets, lib.path) + target=self.add_to_library_keep, args=(assets, self.lib.path) ) # self.add_to_library_keep(assets, lib.path) else: - self._thread = Thread(target=self.add_to_library_split, args=(assets, lib)) + self._thread = Thread( + target=self.add_to_library_split, args=(assets, self.lib) + ) # self.add_to_library_split(assets, lib) context.window_manager.modal_handler_add(self) @@ -195,6 +199,7 @@ def finished(self, context: Context): # try: utils.update_asset_browser_areas(context) + self.lib.save_json() # except Exception as e: # print(f"An error occurred while refreshing the asset library: {e}") @@ -202,6 +207,7 @@ def finished(self, context: Context): def follow_up(self): self.prog.end() + for area in bpy.context.screen.areas: area.tag_redraw() @@ -267,7 +273,6 @@ def add_to_library_split( self.updated = True with lib.open_catalogs_file() as catfile: - catfile: utils.CatalogsFile for catalog in catalogs: catfile.add_catalog_from_other(catalog) diff --git a/ops/asset_ops.py b/ops/asset_ops.py index 26ce504..1105c95 100644 --- a/ops/asset_ops.py +++ b/ops/asset_ops.py @@ -47,7 +47,6 @@ def execute(self, context): asset.catalog_id = cat.id else: with lib.open_catalogs_file() as cat_file: - cat_file: "utils.CatalogsFile" if "/" in catalog_simple_name: name = catalog_simple_name.split("/")[-1] cat = cat_file.add_catalog(name, path=catalog_simple_name) diff --git a/ops/remove_empty_catalogs.py b/ops/remove_empty_catalogs.py index 51cbdd4..cc07747 100644 --- a/ops/remove_empty_catalogs.py +++ b/ops/remove_empty_catalogs.py @@ -33,8 +33,6 @@ def execute(self, context): print("No catalogs used in library") with lib.open_catalogs_file() as cat_file: - cat_file: utils.CatalogsFile - for cat in cat_file.get_catalogs(): if cat.id not in catalog_ids and not cat.has_child(catalog_ids): cat.remove_self() diff --git a/settings/scene.py b/settings/scene.py index 9850a1b..d5175c0 100644 --- a/settings/scene.py +++ b/settings/scene.py @@ -654,7 +654,6 @@ def process_asset_metadata( asset.catalog_id = cat.id if not cat: with lib.open_catalogs_file() as cat_file: - cat_file: "utils.CatalogsFile" if "/" in catalog_simple_name: name = catalog_simple_name.split("/")[-1] cat = cat_file.add_catalog( diff --git a/utils.py b/utils.py index 0ed90ab..d01c17b 100644 --- a/utils.py +++ b/utils.py @@ -1,12 +1,13 @@ # Utilities for interacting with the blender_assets.cats.txt file import functools +import json import subprocess import threading import uuid from contextlib import contextmanager from pathlib import Path from platform import system -from typing import TYPE_CHECKING, Union +from typing import TYPE_CHECKING, Generator, Union import bpy from bpy.types import ID, Area, AssetRepresentation, Context, UserAssetLibrary, Window @@ -324,6 +325,15 @@ def get_catalog_by_name(self, id: str, recursive=True) -> "Catalog": if cat: return cat + def to_dict(self) -> dict: + return { + "id": self.id, + "simple_name": self.simple_name, + "name": self.name, + "path": self.path, + "children": [child.to_dict() for child in self.children.values()], + } + class CatalogsFile: def __init__(self, dir: Path, is_new=False) -> None: @@ -365,9 +375,7 @@ def __init__(self, dir: Path, is_new=False) -> None: self.path = dir / "blender_assets.cats.txt" break if not found_new: - raise FileNotFoundError( - f"Catalogs file not found in directory: {self.path.parent}" - ) + raise FileNotFoundError(f"Catalogs file not found in directory: {self.path.parent}") self.load_catalogs() @@ -420,9 +428,7 @@ def delete_file(self) -> None: if self.exists(): self.path.unlink() - def add_catalog( - self, name: str, id: str = None, path: str = None, auto_place=False - ) -> Catalog: + def add_catalog(self, name: str, id: str = None, path: str = None, auto_place=False) -> Catalog: """ Add a catalog at the root level or under a parent catalog. @@ -612,6 +618,12 @@ def get_catalog_by_name(self, id: str, recursive=True) -> Catalog: if cat: return cat + def to_dict(self) -> dict: + return { + "path": str(self.path), + "catalogs": [cat.to_dict() for cat in self.catalogs.values()], + } + # Context manager for opening and then saving the catalogs file @contextmanager @@ -643,9 +655,10 @@ def __init__(self, asset: AssetRepresentation) -> None: self.catalog_simple_name = asset.metadata.catalog_simple_name """The original `catalog_simple_name` of the asset. Not a new one""" self.catalog_id = asset.metadata.sh_catalog - self.tags = [tag.name for tag in asset.metadata.sh_tags.tags] - self.bpy_tags = [tag.name for tag in asset.metadata.tags] + self.tags: list[str] = [tag.name for tag in asset.metadata.sh_tags.tags] + self.bpy_tags: list[str] = [tag.name for tag in asset.metadata.tags] self.icon_path = None + # self.filepath = asset.metadata.sh_filepath def update_asset(self, blender_exe: str, debug: bool = False) -> None: """Open asset's blend file and update the asset's metadata.""" @@ -679,9 +692,7 @@ def update_asset(self, blender_exe: str, debug: bool = False) -> None: print("".center(100, "-")) text = proc.stdout.decode() text.splitlines() - new_text = "\n".join( - line for line in text.splitlines() if line.startswith("|") - ) + new_text = "\n".join(line for line in text.splitlines() if line.startswith("|")) print(new_text) print("".center(100, "-")) print() @@ -727,9 +738,7 @@ def reset_metadata(self, context: Context) -> None: sh_tags.new_tag(tag.name, context) self.orig_asset.metadata.sh_is_dirty_tags = False - def rerender_thumbnail( - self, path, directory, objects, shading, angle="X", add_plane=False - ): + def rerender_thumbnail(self, path, directory, objects, shading, angle="X", add_plane=False): prefs = get_prefs() cmd = [bpy.app.binary_path] # cmd.append("--background") @@ -738,11 +747,7 @@ def rerender_thumbnail( cmd.append("--python") # cmd.append(os.path.join(os.path.dirname( # os.path.abspath(__file__)), "rerender_thumbnails.py")) - cmd.append( - str( - Path(__file__).parent / "stand_alone_scripts" / "rerender_thumbnails.py" - ) - ) + cmd.append(str(Path(__file__).parent / "stand_alone_scripts" / "rerender_thumbnails.py")) cmd.append("--") cmd.append(":--separator--:".join(path)) names = [] @@ -770,30 +775,6 @@ def rerender_thumbnail( subprocess.run(cmd) return None - def save_out_preview(self, directory: Path) -> None: - """Save out the preview image of the asset.""" - python_file = ( - Path(__file__).parent / "stand_alone_scripts" / "save_out_previews.py" - ) - - args = [ - bpy.app.binary_path, - "-b", - "--factory-startup", - str(self.orig_asset.full_library_path), - "-P", - str(python_file), - str(directory), - self.orig_asset.name, - ASSET_TYPES_TO_ID_TYPES.get(self.id_type), - "False", - ] - - proc = subprocess.run(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - if proc.returncode > 1: - print(f"Error: {proc.stderr.decode()}") - print(f"Output: {proc.stdout.decode()}") - class Assets: def __init__(self, assets: list[AssetRepresentation]) -> None: @@ -805,6 +786,9 @@ def __init__(self, assets: list[AssetRepresentation]) -> None: self._dict[a.name] = a self._list.append(a) + def to_dict(self, apply_changes=False) -> list[dict]: + return [asset.to_dict(apply_changes=apply_changes) for asset in self._list] + def __getitem__(self, key_or_index: str | int) -> Asset: if isinstance(key_or_index, str): return self._dict[key_or_index] @@ -845,8 +829,7 @@ def __init__( area for window in context.window_manager.windows for area in window.screen.areas - if area.type == "FILE_BROWSER" - and asset_utils.SpaceAssetInfo.is_asset_browser(area.spaces.active) + if area.type == "FILE_BROWSER" and asset_utils.SpaceAssetInfo.is_asset_browser(area.spaces.active) ), None, ) @@ -868,9 +851,7 @@ def __init__( def get_context(self) -> Context: if not self.context: - raise ValueError( - "Context not set. Please set `context` before calling this method." - ) + raise ValueError("Context not set. Please set `context` before calling this method.") return self.context def load_catalogs(self): @@ -887,7 +868,7 @@ def load_assets(self): self.assets = Assets(self.get_possible_assets()) @contextmanager - def open_catalogs_file(self) -> CatalogsFile: + def open_catalogs_file(self) -> Generator[CatalogsFile, None, None]: if not self.catalogs: yield None else: @@ -897,9 +878,7 @@ def open_catalogs_file(self) -> CatalogsFile: @classmethod def create_bpy_library(cls, name: str, path: str) -> UserAssetLibrary: """Create a new UserAssetLibrary in Blender.""" - return bpy.context.preferences.filepaths.asset_libraries.new( - name=name, directory=path - ) + return bpy.context.preferences.filepaths.asset_libraries.new(name=name, directory=path) @classmethod def create_new_library( @@ -934,11 +913,7 @@ def save_repository_prefs(cls, name: str, path: str): if not prefs_blend.exists(): return - python_file = ( - Path(__file__).parent - / "stand_alone_scripts" - / "add_repository_to_userpref.py" - ) + python_file = Path(__file__).parent / "stand_alone_scripts" / "add_repository_to_userpref.py" args = [ bpy.app.binary_path, @@ -953,18 +928,38 @@ def save_repository_prefs(cls, name: str, path: str): subprocess.run(args) + def to_dict(self, apply_changes=False) -> dict: + # modeled after https://projects.blender.org/blender/blender/issues/125597 + return { + "name": self.name, + "schema_version": 1, + "asset_size_bytes": 0, # TODO: Implement + "index_size_bytes": 0, # TODO: Implement + "path": str(self.path), + "assets": (self.assets.to_dict(apply_changes=apply_changes) if self.assets else []), + "catalogs": self.catalogs.to_dict() if self.catalogs else [], + } + + def save_json(self) -> None: + json_path = self.path / "library.json" + if json_path.exists(): + data = json.loads(json_path.read_text()) + data["libaries"] = self.to_dict() + else: + data = self.to_dict() + + with open(json_path, "w") as file: + json.dump(data, file, indent=4) -def get_active_bpy_library_from_context( - context: Context, area: Area = None -) -> UserAssetLibrary: + +def get_active_bpy_library_from_context(context: Context, area: Area = None) -> UserAssetLibrary: if not area: area = next( ( area for window in context.window_manager.windows for area in window.screen.areas - if area.type == "FILE_BROWSER" - and asset_utils.SpaceAssetInfo.is_asset_browser(area.spaces.active) + if area.type == "FILE_BROWSER" and asset_utils.SpaceAssetInfo.is_asset_browser(area.spaces.active) ), None, ) @@ -976,21 +971,15 @@ def get_active_bpy_library_from_context( return context.preferences.filepaths.asset_libraries.get(lib_name) -def from_name( - name: str, context: Context = None, load_assets=False, load_catalogs=False -) -> AssetLibrary: +def from_name(name: str, context: Context = None, load_assets=False, load_catalogs=False) -> AssetLibrary: """Gets a library by name and returns an AssetLibrary object.""" lib = bpy.context.preferences.filepaths.asset_libraries.get(name) if not lib: raise ValueError(f"Library with name '{name}' not found.") - return AssetLibrary( - lib, context=context, load_assets=load_assets, load_catalogs=load_catalogs - ) + return AssetLibrary(lib, context=context, load_assets=load_assets, load_catalogs=load_catalogs) -def from_active( - context: Context, area: Area = None, load_assets=False, load_catalogs=False -) -> AssetLibrary: +def from_active(context: Context, area: Area = None, load_assets=False, load_catalogs=False) -> AssetLibrary: """Gets the active library from the UI context and returns an AssetLibrary object.""" return AssetLibrary( get_active_bpy_library_from_context(context, area=area), @@ -1151,9 +1140,7 @@ def rerender_thumbnail( print("*" * 115) print("Thread Starting".center(100, "*")) print("*" * 115) - python_file = ( - Path(__file__).parent / "stand_alone_scripts" / "rerender_thumbnails.py" - ) + python_file = Path(__file__).parent / "stand_alone_scripts" / "rerender_thumbnails.py" names = [] types = [] @@ -1247,14 +1234,8 @@ def rerender_thumbnail( for i, tbp in enumerate(thumbnail_blends): orig_stem = tbp.stem.split("=+=")[0] orig_blend_path = tbp.with_stem(orig_stem) - thumbnail_path = ( - orig_blend_path.parent - / f"{tbp.stem.replace('_thumbnail_copy', '')}_thumbnail_1.png" - ) - thumbnail_path_for_terminal = ( - orig_blend_path.parent - / f"{tbp.stem.replace('_thumbnail_copy', '')}_thumbnail_#" - ) + thumbnail_path = orig_blend_path.parent / f"{tbp.stem.replace('_thumbnail_copy', '')}_thumbnail_1.png" + thumbnail_path_for_terminal = orig_blend_path.parent / f"{tbp.stem.replace('_thumbnail_copy', '')}_thumbnail_#" args = [ bpy.app.binary_path, "-b", @@ -1274,9 +1255,7 @@ def rerender_thumbnail( print(" - exists", tbp.exists()) print("CMD:", " ".join(args)) try: - proc: subprocess.CompletedProcess = subprocess.run( - args, stdout=subprocess.PIPE, check=True - ) + proc: subprocess.CompletedProcess = subprocess.run(args, stdout=subprocess.PIPE, check=True) proc.returncode except subprocess.CalledProcessError as e: print(f"- Error: {e}") @@ -1422,10 +1401,7 @@ def mouse_in_window(window: Window, x, y) -> bool: Returns: bool: True if the mouse is within the window boundaries, False otherwise. """ - return ( - window.x <= x <= window.x + window.width - and window.y <= y <= window.y + window.height - ) + return window.x <= x <= window.x + window.width and window.y <= y <= window.y + window.height def pack_files(blend_file: Path): @@ -1501,9 +1477,7 @@ def export_helper( str(destination_dir), ] - proc = subprocess.Popen( - args, stderr=subprocess.STDOUT, stdout=subprocess.PIPE, text=True - ) + proc = subprocess.Popen(args, stderr=subprocess.STDOUT, stdout=subprocess.PIPE, text=True) # proc = subprocess.Popen(args) if op: @@ -1552,9 +1526,7 @@ def export_helper( print(f"Output: {proc.stdout.decode()}") -def update_asset_browser_areas( - context: Context = None, tag_redraw=True, update_library=True -): +def update_asset_browser_areas(context: Context = None, tag_redraw=True, update_library=True): C = context or bpy.context for area in C.screen.areas: @@ -1613,15 +1585,9 @@ def mark_assets_in_blend( Key: Blend File Path Value: Tuple of (Asset Name, Asset Type) """ - python_file = ( - Path(__file__).parent / "stand_alone_scripts" / "mark_assets_in_blend_file.py" - ) + python_file = Path(__file__).parent / "stand_alone_scripts" / "mark_assets_in_blend_file.py" - blends = ( - list(Path(directory).rglob("**/*.blend")) - if recursive - else list(Path(directory).glob("*.blend")) - ) + blends = list(Path(directory).rglob("**/*.blend")) if recursive else list(Path(directory).glob("*.blend")) tags_to_add = [] for tag_name, tag_bool in zip(hive_mind.TAGS_DICT.keys(), tags):