diff --git a/__init__.py b/__init__.py index 505dc95..993149b 100644 --- a/__init__.py +++ b/__init__.py @@ -16,14 +16,18 @@ bl_info = { "category": "Import-Export", } +import sys -from . import operators, properties, ui, preferences +from . import operators, properties, ui, preferences, data_type +from .core.lib_utils import load_libraries, update_library_path -modules = ( + +bl_modules = ( operators, properties, ui, - preferences + preferences, + data_type ) # Reload Modules from inside Blender @@ -33,16 +37,33 @@ if "bpy" in locals(): for mod in modules: importlib.reload(mod) +import bpy + + +def load_handler(): + print('load_handler') + load_libraries() + update_library_path() + #set_env_libraries() + #bpy.ops.assetlib.set_paths(all=True) + + #if not bpy.app.background: + # bpy.ops.assetlib.bundle(blocking=False, mode='AUTO_BUNDLE') + + def register(): """Register the addon Asset Library for Blender""" - for mod in modules: + for mod in bl_modules: mod.register() + + + bpy.app.timers.register(load_handler, first_interval=1) def unregister(): """Unregister the addon Asset Library for Blender""" - for mod in reversed(modules): + for mod in reversed(bl_modules): mod.unregister() \ No newline at end of file diff --git a/constants.py b/constants.py index b5db4bb..3103b8c 100644 --- a/constants.py +++ b/constants.py @@ -3,10 +3,18 @@ import bpy DATA_TYPE_ITEMS = [ - ("ACTION", "Action", "", "ACTION", 0), - ("COLLECTION", "Collection", "", "OUTLINER_OB_GROUP_INSTANCE", 1), - ("FILE", "File", "", "FILE", 2) + ("NodeTree", "Node Group", "", "NODETREE", 0), + ("Material", "Material", "", "MATERIAL", 1), + ("Object", "Object", "", "OBJECT_DATA", 2), + ("Action", "Action", "", "ACTION", 3), + ("Collection", "Collection", "", "OUTLINER_OB_GROUP_INSTANCE", 4), + ("File", "File", "", "FILE", 5) ] + +DATA_TYPE_GEO_ITEMS = [DATA_TYPE_ITEMS[0], DATA_TYPE_ITEMS[2]] +DATA_TYPE_SHADING_ITEMS = [DATA_TYPE_ITEMS[0], DATA_TYPE_ITEMS[1]] + +CATALOG_ITEMS = {} DATA_TYPES = [i[0] for i in DATA_TYPE_ITEMS] ICONS = {identifier: icon for identifier, name, description, icon, number in DATA_TYPE_ITEMS} @@ -15,9 +23,14 @@ MODULE_DIR = Path(__file__).parent RESOURCES_DIR = MODULE_DIR / 'resources' PLUGINS_DIR = MODULE_DIR / 'plugins' -PLUGINS = set() +PLUGINS = {} PLUGINS_ITEMS = [('NONE', 'None', '', 0)] +LIB_DIR = MODULE_DIR / 'libs' +LIB_ITEMS = [] + +SCRIPTS_DIR = MODULE_DIR / 'scripts' + PREVIEW_ASSETS_SCRIPT = MODULE_DIR / 'common' / 'preview_assets.py' #ADD_ASSET_DICT = {} diff --git a/core/asset_library_utils.py b/core/asset_library_utils.py deleted file mode 100644 index c1c7560..0000000 --- a/core/asset_library_utils.py +++ /dev/null @@ -1,54 +0,0 @@ -""" -Util function for this addon -""" - -import bpy -from . bl_utils import get_addon_prefs - - -def thumbnail_blend_file(input_blend, output_img): - input_blend = Path(input_blend).resolve() - output_img = Path(output_img).resolve() - - print(f'Thumbnailing {input_blend} to {output_img}') - blender_thumbnailer = Path(bpy.app.binary_path).parent / 'blender-thumbnailer' - - output_img.parent.mkdir(exist_ok=True, parents=True) - - subprocess.call([blender_thumbnailer, str(input_blend), str(output_img)]) - - success = output_img.exists() - - if not success: - empty_preview = RESOURCES_DIR / 'empty_preview.png' - shutil.copy(str(empty_preview), str(output_img)) - - return success - - -def get_active_library(): - '''Get the pref library properties from the active library of the asset browser''' - prefs = get_addon_prefs() - lib_ref = bpy.context.space_data.params.asset_library_reference - - #Check for merged library - for l in prefs.libraries: - if l.name == lib_ref: - return l - - -def update_library_path(self, context): - """Removing all asset libraries and recreate them""" - - addon_prefs = get_addon_prefs() - libs = context.preferences.filepaths.asset_libraries - - for i, lib in reversed(list(enumerate(libs))): - if (addon_lib := addon_prefs.libraries.get(lib.name)) and addon_lib.path == lib.path: - bpy.ops.preferences.asset_library_remove(index=i) - - for addon_lib in addon_prefs.libraries: - if not addon_lib.use: - continue - bpy.ops.preferences.asset_library_add(directory=str(addon_lib.path)) - libs[-1].name = addon_lib.name \ No newline at end of file diff --git a/core/bl_utils.py b/core/bl_utils.py index 5e8358c..074dce2 100644 --- a/core/bl_utils.py +++ b/core/bl_utils.py @@ -2,7 +2,7 @@ """ Generic Blender functions """ - +import json from pathlib import Path from fnmatch import fnmatch from typing import Any, List, Iterable, Optional, Tuple @@ -15,6 +15,8 @@ from bpy_extras import asset_utils from os.path import abspath import subprocess +from .file_utils import norm_str + class attr_set(): '''Receive a list of tuple [(data_path, "attribute" [, wanted value)] ] @@ -82,6 +84,9 @@ def get_viewport(): screen = bpy.context.screen areas = [a for a in screen.areas if a.type == 'VIEW_3D'] + if not areas: + return + areas.sort(key=lambda x : x.width*x.height) return areas[-1] @@ -216,7 +221,7 @@ def norm_arg(arg_name, format=str.lower, prefix='--', separator='-'): return prefix + arg_name -def get_bl_cmd(blender=None, background=False, focus=True, blendfile=None, script=None, **kargs): +def get_bl_cmd(blender=None, background=False, factory_startup=False, focus=True, blendfile=None, script=None, **kargs): cmd = [str(blender)] if blender else [bpy.app.binary_path] if background: @@ -228,6 +233,9 @@ def get_bl_cmd(blender=None, background=False, focus=True, blendfile=None, scrip cmd += ['--python-use-system-env'] + if factory_startup: + cmd += ['--factory-startup'] + if blendfile: cmd += [str(blendfile)] @@ -347,7 +355,10 @@ def split_path(path) : return bone_name, prop_name - +def get_asset_type(asset_type): + data_types = { p.fixed_type.identifier: p.identifier for p in + bpy.types.BlendData.bl_rna.properties if hasattr(p, 'fixed_type')} + return data_types[asset_type] def load_datablocks(src, names=None, type='objects', link=True, expr=None, assets_only=False) -> list: @@ -482,4 +493,86 @@ def get_object_libraries(ob): filepaths.append(absolute_filepath) - return filepaths \ No newline at end of file + return filepaths + + +def clean_name(name): + if re.match(r'(.*)\.\d{3}$', name): + return name[:-4] + return name + + +def is_node_groups_duplicate(node_groups): + node_group_types = sorted([n.type for n in node_groups[0].nodes]) + return all( sorted([n.type for n in ng.nodes]) == + node_group_types for ng in node_groups[1:]) + + +def is_images_duplicate(images): + return all( img.filepath == images[0].filepath for image in images) + + +def is_materials_duplicate(materials): + node_group_types = sorted([n.type for n in materials[0].node_tree.nodes]) + return all( sorted([n.type for n in mat.node_tree.nodes]) == + node_group_types for mat in materials[1:]) + + +def merge_datablock_duplicates(datablocks, blend_data, force=False): + """Merging materials, node_groups or images based on name .001, .002""" + + failed = [] + merged = [] + + datablocks = list(datablocks) + + #blend_data = get_asset_type(datablocks[0].bl_rna.identifier) + if blend_data == 'materials': + is_datablock_duplicate = is_materials_duplicate + elif blend_data == 'node_groups': + is_datablock_duplicate = is_node_groups_duplicate + elif blend_data == 'images': + is_datablock_duplicate = is_images_duplicate + else: + raise Exception(f'Type, {blend_data} not supported') + + # Group by name + groups = {} + for datablock in images: + groups.setdefault(clean_name(datablock.name), []).append(datablock) + + for datablock in blend_data: + name = clean_name(datablock.name) + if name in groups and datablock not in groups[name]: + groups[name].append(datablock) + + print("\nMerge Duplicate Datablocks...") + + for group in groups.values(): + if len(group) == 1: + continue + + if not force: + datablocks.sort(key=lambda x : x.name, reverse=True) + + for datablock in datablocks[1:]: + is_duplicate = is_datablock_duplicate((datablock, datablocks[0])) + + if not is_duplicate and not force: + failed.append((datablock.name, datablocks[0].name)) + print(f'Cannot merge Datablock {datablocks.name} with {datablocks[0].name} they are different') + continue + + merged.append((datablock.name, datablocks[0].name)) + print(f'Merge Datablock {datablock.name} into {datablocks[0].name}') + + datablock.user_remap(datablocks[0]) + datablocks.remove(datablock) + blend_data.remove(datablock) + + # Rename groups if it has no duplicate left + for datablocks in groups.values(): + if len(datablocks) == 1 and not datablocks[0].library: + datablocks[0].name = clean_name(datablocks[0].name) + + return merged, failed \ No newline at end of file diff --git a/core/catalog.py b/core/catalog.py index 3bdcd68..0701dfe 100644 --- a/core/catalog.py +++ b/core/catalog.py @@ -2,6 +2,14 @@ from pathlib import Path import uuid import bpy +from .file_utils import cache + +@cache(1) +def read_catalog(library_path): + catalog = Catalog(library_path) + catalog.read() + + return catalog class CatalogItem: diff --git a/core/file_utils.py b/core/file_utils.py index 3286202..56d8a7c 100644 --- a/core/file_utils.py +++ b/core/file_utils.py @@ -12,9 +12,12 @@ from pathlib import Path import importlib import sys import shutil +from functools import wraps +from time import perf_counter import contextlib + @contextlib.contextmanager def cd(path): """Changes working directory and returns to previous on exit.""" @@ -25,6 +28,7 @@ def cd(path): finally: os.chdir(prev_cwd) + def install_module(module_name, package_name=None): '''Install a python module with pip or return it if already installed''' try: @@ -39,6 +43,7 @@ def install_module(module_name, package_name=None): return module + def import_module_from_path(path): from importlib import util @@ -54,6 +59,7 @@ def import_module_from_path(path): print(f'Cannot import file {path}') print(e) + def norm_str(string, separator='_', format=str.lower, padding=0): string = str(string) string = string.replace('_', ' ') @@ -73,6 +79,7 @@ def norm_str(string, separator='_', format=str.lower, padding=0): return string + def remove_version(filepath): pattern = '_v[0-9]+\.' search = re.search(pattern, filepath) @@ -82,12 +89,14 @@ def remove_version(filepath): return Path(filepath).name + def is_exclude(name, patterns) -> bool: # from fnmatch import fnmatch if not isinstance(patterns, (list,tuple)) : patterns = [patterns] return any([fnmatch(name, p) for p in patterns]) + def get_last_files(root, pattern=r'_v\d{3}\.\w+', only_matching=False, ex_file=None, ex_dir=None, keep=1, verbose=False) -> list: '''Recursively get last(s) file(s) (when there is multiple versions) in passed directory root -> str: Filepath of the folder to scan. @@ -139,6 +148,7 @@ def get_last_files(root, pattern=r'_v\d{3}\.\w+', only_matching=False, ex_file=N return sorted(files) + def copy_file(src, dst, only_new=False, only_recent=False): if dst.exists(): if only_new: @@ -153,6 +163,7 @@ def copy_file(src, dst, only_new=False, only_recent=False): else: subprocess.call(['cp', str(src), str(dst)]) + def copy_dir(src, dst, only_new=False, only_recent=False, excludes=['.*'], includes=[]): src, dst = Path(src), Path(dst) @@ -203,6 +214,7 @@ def open_file(filepath, select=False): cmd += [str(filepath)] subprocess.Popen(cmd) + def open_blender_file(filepath=None): filepath = filepath or bpy.data.filepath @@ -217,6 +229,32 @@ def open_blender_file(filepath=None): subprocess.Popen(cmd) + +def cache(timeout): + _cache = {} + + def decorator(func): + @wraps(func) + def wrapper(*args, **kwargs): + current_time = perf_counter() + cache_key = (*args, *kwargs.items()) # Assuming the first argument is the file path + + # Check if the cache is valid + if cache_key in _cache: + cached_content, cache_time = _cache[cache_key] + if (current_time - cache_time) < timeout: + + return cached_content + + # Execute the function and update the cache + result = func(*args, **kwargs) + _cache[cache_key] = (result, current_time) + return result + + return wrapper + return decorator + + def read_file(path): '''Read a file with an extension in (json, yaml, yml, txt)''' @@ -255,6 +293,7 @@ def read_file(path): return data + def write_file(path, data, indent=4): '''Read a file with an extension in (json, yaml, yml, text)''' @@ -315,4 +354,4 @@ def synchronize(src, dst, only_new=False, only_recent=False, clear=False): except Exception as e: print(e) - \ No newline at end of file + diff --git a/core/lib_utils.py b/core/lib_utils.py new file mode 100644 index 0000000..636f524 --- /dev/null +++ b/core/lib_utils.py @@ -0,0 +1,319 @@ +""" +Util function for this addon +""" + +import os +from pathlib import Path +import inspect +from datetime import datetime +from time import perf_counter +import platform + +import bpy +from .catalog import Catalog, read_catalog +from .bl_utils import get_addon_prefs, get_asset_type +from .file_utils import read_file, write_file, cache, import_module_from_path + + +def thumbnail_blend_file(input_blend, output_img): + input_blend = Path(input_blend).resolve() + output_img = Path(output_img).resolve() + + print(f'Thumbnailing {input_blend} to {output_img}') + blender_thumbnailer = Path(bpy.app.binary_path).parent / 'blender-thumbnailer' + + output_img.parent.mkdir(exist_ok=True, parents=True) + + subprocess.call([blender_thumbnailer, str(input_blend), str(output_img)]) + + success = output_img.exists() + + if not success: + empty_preview = RESOURCES_DIR / 'empty_preview.png' + shutil.copy(str(empty_preview), str(output_img)) + + return success + + +def get_active_library(): + '''Get the pref library properties from the active library of the asset browser''' + prefs = get_addon_prefs() + lib_ref = bpy.context.space_data.params.asset_library_reference + + #Check for merged library + for l in prefs.libraries: + if l.name == lib_ref: + return l + + +def update_library_path(): + """Removing all asset libraries and recreate them""" + + print("update_library_path") + + addon_prefs = get_addon_prefs() + libs = bpy.context.preferences.filepaths.asset_libraries + + for i, lib in reversed(list(enumerate(libs))): + if (addon_lib := addon_prefs.libraries.get(lib.name)): # and addon_lib.path == lib.path + bpy.ops.preferences.asset_library_remove(index=i) + + for addon_lib in addon_prefs.libraries: + if not addon_lib.use: + continue + bpy.ops.preferences.asset_library_add(directory=str(addon_lib.path)) + libs[-1].name = addon_lib.name + + +def load_library_config(config_path): + """"Load library prefs from config path""" + if not config_path: + return [] + + config_path = Path(config_path) + + if not config_path.exists(): + print(f'Config {config_path} not exist') + return [] + + prefs = get_addon_prefs() + + libs = [] + for lib_dict in read_file(config_path): + lib = prefs.libraries.add() + lib.is_user = False + lib.set_dict(lib_dict) + libs.append(lib) + + return libs + + +def load_libraries(): + """"Load library prefs from config pref and env""" + prefs = get_addon_prefs() + + bl_libs = bpy.context.preferences.filepaths.asset_libraries + for i, bl_lib in reversed(list(enumerate(bl_libs))): + addon_lib = prefs.libraries.get(bl_lib.name) + if not addon_lib or addon_lib.is_user: + continue + + bpy.ops.preferences.asset_library_remove(index=i) + + # Remove lib from addons preferences + for i, addon_lib in reversed(list(enumerate(prefs.libraries))): + if not addon_lib.is_user: + prefs.libraries.remove(i) + + env_config = os.getenv('ASSET_LIBRARY_CONFIG') + libs = load_library_config(env_config) + load_library_config(prefs.config_path) + + return libs + + +def clear_time_tag(asset): + # Created time tag + for tag in list(asset.asset_data.tags): + try: + datetime.strptime(tag.name, "%Y-%m-%d %H:%M") + asset.asset_data.tags.remove(tag) + except ValueError: + continue + + +def create_time_tag(asset): + asset.asset_data.tags.new(datetime.now().strftime("%Y-%m-%d %H:%M")) + + +def version_file(path, save_versions=3): + if path.exists(): + for i in range(save_versions): + version = save_versions - i + version_path = path.with_suffix(f'.blend{version}') + if not version_path.exists(): + continue + if i == 0: + version_path.unlink() + else: + version_path.rename(path.with_suffix(f'.blend{version+1}')) + + path.rename(path.with_suffix(f'.blend1')) + + +def list_datablocks(blend_file, asset_types={"objects", "materials", "node_groups"}): + blend_data = {} + with bpy.data.temp_data(filepath=str(blend_file)) as temp_data: + with temp_data.libraries.load(str(blend_file), link=True) as (data_from, data_to): + for asset_type in asset_types: + blend_data[asset_type] = getattr(data_from, asset_type) + + return blend_data + + +def get_asset_data(blend_file, asset_type, name, preview=False): + with bpy.data.temp_data(filepath=str(blend_file)) as temp_data: + with temp_data.libraries.load(str(blend_file), link=True) as (data_from, data_to): + if name not in getattr(data_from, asset_type): + return + setattr(data_to, asset_type, [name]) + + if assets := getattr(data_to, asset_type): + asset = assets[0] + asset_data = asset.asset_data + + data = { + "description": asset_data.description, + "catalog_id": asset_data.catalog_id, + "catalog_simple_name": asset_data.catalog_simple_name, + "path": blend_file, + "tags": list(asset_data.tags.keys()) + } + + if asset.preview and preview: + image_size = asset.preview.image_size + preview_pixels = [0] * image_size[0] * image_size[1] * 4 + asset.preview.image_pixels_float.foreach_get(preview_pixels) + + data["preview_pixels"] = preview_pixels + data["preview_size"] = list(image_size) + + return data + + +def find_asset_data(name, asset_type, preview=False): + """Find info about an asset found in library""" + + bl_libs = bpy.context.preferences.filepaths.asset_libraries + + # First search for a blend with the same name + for bl_lib in bl_libs: + for blend_file in Path(bl_lib.path).glob(f"**/{name}.blend"): + if asset_data := get_asset_data(blend_file, asset_type, name, preview=preview): + asset_data['library'] = bl_lib + return asset_data + + # for bl_lib in bl_libs: + # for blend_file in Path(bl_lib.path).glob("**/*.blend"): + # if asset_data := get_asset_data(blend_file, asset_type, name): + # return bl_lib, asset_data + + +def get_filepath_library(filepath): + for lib in bpy.context.preferences.filepaths.asset_libraries: + if bpy.path.is_subdir(filepath, lib.path): + return lib + + +def get_asset_catalog_path(asset, fallback=''): + if asset.local_id: + path = Path(bpy.data.filepath).parent + + elif (lib := get_filepath_library(asset.full_library_path)): + if lib: + path = lib.path + else: + return fallback + + if not (catalog := read_catalog(path)): + return fallback + + if not (catalog_item := catalog.get(id=asset.metadata.catalog_id, fallback=fallback)): + return fallback + + return catalog_item.path + + +def get_asset_full_path(asset): + """Get a path that represent all informations about an asset path/type/catalog/name""" + + asset_path = asset.full_library_path + if asset.local_id: + asset_path = f'{bpy.data.filepath}/{asset_path}' + + asset_type, asset_name = Path(asset.full_path).parts[-2:] + asset_type = get_asset_type(asset_type) + + return Path(asset_path, asset_type, asset_name).as_posix() + + +def get_asset_source(datablock): + weak_reference = datablock.library_weak_reference + if isinstance(datablock, bpy.types.Object) and datablock.data: + weak_reference = datablock.data.library_weak_reference + + if weak_reference and (source_path := Path(weak_reference.filepath)).exists(): + return source_path + + asset_libraries = context.preferences.filepaths.asset_libraries + for asset_library in asset_libraries: + library_path = Path(asset_library.path) + if blend_files := list(library_path.glob(f"**/{datablock.name}.blend")): + return + + return datablock.library_weak_reference + + +def get_blender_cache_dir(): + if platform.system() == 'Linux': + cache_folder = os.path.expandvars('$HOME/.cache/blender') + elif platform.system() == 'Windows': + cache_folder = os.path.expanduser('%USERPROFILE%/AppData/Local/Blender Foundation/Blender') + elif platform.system() == 'Darwin': + cache_folder = '/Library/Caches/Blender' + + return Path(cache_folder) + + +def find_asset_source(library_map, asset_type, name): + return next( (l for l, blend_data in sorted(library_map.items(), key=lambda x: x[1]['st_mtime'], reverse=True) + if name in blend_data['node_groups']), None) + + +def asset_library_map(): + """"Get a mapping of all datablocks of the blend files from the libraries""" + asset_libraries = bpy.context.preferences.filepaths.asset_libraries + + cache_file = get_blender_cache_dir() / 'asset-library.json' + cache = None + if cache_file.exists(): + cache = read_file(cache_file) + + if cache is None: + cache = {} + + file_keys = [] + + for asset_library in asset_libraries: + library_path = Path(asset_library.path) + + for bl_file in library_path.glob("**/*.blend"): + file_keys.append(file_key := bl_file.as_posix()) + + st_mtime = bl_file.stat().st_mtime + if (bl_cache := cache.get(file_key)) and bl_cache.get('st_mtime') >= st_mtime: + continue + + datablocks = list_datablocks(bl_file) + cache[file_key] = dict(st_mtime=st_mtime, **datablocks) + + # Remove map when the blend not exists anymore + for file_key in list(cache.keys()): + if file_key not in file_keys: + del cache[file_key] + + write_file(cache_file, cache) + + return cache + +# print(perf_counter() - t0) + + # t0 = perf_counter() + # for asset_library in asset_libraries: + # library_path = Path(asset_library.path) + + # for blend_file in library_path.glob("**/*.blend"): + # with bpy.data.libraries.load(str(blend_file), link=True) as (data_from, data_to): + # node_groups = data_from.node_groups + # print(node_groups) + + # print(perf_counter() - t0) \ No newline at end of file diff --git a/data_type/__init__.py b/data_type/__init__.py index e69de29..b5d58d4 100644 --- a/data_type/__init__.py +++ b/data_type/__init__.py @@ -0,0 +1,26 @@ + +from . node import ui as node_ui +from . node import operator as node_operator +from . material import ui as material_ui +from . material import operator as material_operator + +bl_modules = ( + node_ui, + node_operator, + material_ui, + material_operator +) + + +def register(): + """Register the addon Asset Library for Blender""" + + for mod in bl_modules: + mod.register() + + +def unregister(): + """Unregister the addon Asset Library for Blender""" + + for mod in reversed(bl_modules): + mod.unregister() \ No newline at end of file diff --git a/data_type/action/__init__.py b/data_type/action/__init__.py index b55d078..48f83ee 100644 --- a/data_type/action/__init__.py +++ b/data_type/action/__init__.py @@ -1,26 +1,9 @@ -from asset_library.action import ( - gui, +from asset_library.data_type.action import ( keymaps, - clear_asset, - concat_preview, operators, - properties, - rename_pose, - #render_preview ) -if 'bpy' in locals(): - import importlib - - importlib.reload(gui) - importlib.reload(keymaps) - importlib.reload(clear_asset) - importlib.reload(concat_preview) - importlib.reload(operators) - importlib.reload(properties) - importlib.reload(rename_pose) - #importlib.reload(render_preview) import bpy diff --git a/data_type/action/_render_preview.py b/data_type/action/_render_preview.py index c5436d7..2ca9e1f 100644 --- a/data_type/action/_render_preview.py +++ b/data_type/action/_render_preview.py @@ -4,9 +4,9 @@ import sys from pathlib import Path #sys.path.append(str(Path(__file__).parents[3])) -from asset_library.action.concat_preview import mosaic_export +from asset_library.data_type.action.concat_preview import mosaic_export from asset_library.common.file_utils import open_file -from asset_library.action.functions import reset_bone, get_keyframes +from asset_library.data_type.action.functions import reset_bone, get_keyframes from asset_library.common.functions import read_catalog import bpy diff --git a/data_type/action/operators.py b/data_type/action/operators.py index 0e2ed30..56bb85b 100644 --- a/data_type/action/operators.py +++ b/data_type/action/operators.py @@ -34,7 +34,7 @@ from asset_library.pose.pose_usage import( flip_side_name ) -from asset_library.action.functions import( +from asset_library.data_type.action.functions import( apply_anim, append_action, clean_action, @@ -43,29 +43,16 @@ from asset_library.action.functions import( conform_action ) -from bpy.props import ( - BoolProperty, - CollectionProperty, - EnumProperty, - PointerProperty, - StringProperty, - IntProperty -) +from bpy.props import (BoolProperty, CollectionProperty, EnumProperty, + PointerProperty, StringProperty, IntProperty) -from bpy.types import ( - Action, - Context, - Event, - FileSelectEntry, - Object, - Operator, - PropertyGroup, -) +from bpy.types import (Action, Context, Event, FileSelectEntry, Object, + Operator, PropertyGroup) from bpy_extras import asset_utils from bpy_extras.io_utils import ExportHelper, ImportHelper -from asset_library.action.functions import ( +from asset_library.data_type.action.functions import ( is_pose, get_marker, get_keyframes, diff --git a/data_type/collection/__init__.py b/data_type/collection/__init__.py index 9154052..11d9580 100644 --- a/data_type/collection/__init__.py +++ b/data_type/collection/__init__.py @@ -1,5 +1,5 @@ -from asset_library.collection import ( +from asset_library.data_type.collection import ( gui, operators, keymaps, diff --git a/data_type/file/__init__.py b/data_type/file/__init__.py index 1eed34c..b479ff9 100644 --- a/data_type/file/__init__.py +++ b/data_type/file/__init__.py @@ -1,5 +1,5 @@ -from asset_library.file import ( +from asset_library.data_type.file import ( operators, gui, keymaps) if 'bpy' in locals(): diff --git a/data_type/material/__init__.py b/data_type/material/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/data_type/material/operator.py b/data_type/material/operator.py new file mode 100644 index 0000000..d87fcd1 --- /dev/null +++ b/data_type/material/operator.py @@ -0,0 +1,107 @@ + +from pathlib import Path +from datetime import datetime +import subprocess + +from ...core.bl_utils import get_bl_cmd +from ...core.lib_utils import get_asset_data +from ...core.catalog import read_catalog +from ... import constants + +import bpy +from bpy.types import Operator +from bpy.props import StringProperty, EnumProperty + + +class ASSETLIB_OT_update_materials(Operator): + bl_idname = 'assetlibrary.update_materials' + bl_label = 'Update node' + bl_options = {"REGISTER", "UNDO"} + + selection : EnumProperty(items=[(s, s.title(), '') for s in ('ALL', 'OBJECT', 'CURRENT')], default="CURRENT") + + @classmethod + def poll(cls, context): + return context.object and context.object.active_material + + def invoke(self, context, event): + return context.window_manager.invoke_props_dialog(self) + + def execute(self, context): + asset_libraries = context.preferences.filepaths.asset_libraries + + ob = bpy.context.object + ntree = context.space_data.edit_tree + ntree_name = ntree.name + new_ntree = None + + if self.selection == 'OBJECT': + materials = [s.material for s in ob.material_slots if s.material] + elif self.selection == 'CURRENT': + materials = [ob.active_material] + else: + materials = list(bpy.data.materials) + + mat_names = set(m.name for m in materials) + + for asset_library in asset_libraries: + library_path = Path(asset_library.path) + blend_files = [fp for fp in library_path.glob("**/*.blend") if fp.is_file()] + + images = list(bpy.data.images) + materials = list(bpy.data.materials)# Storing original materials to compare with imported ones + + link = (asset_library.import_method == 'LINK') + for blend_file in blend_files: + print(blend_file) + with bpy.data.libraries.load(str(blend_file), assets_only=True, link=link) as (data_from, data_to): + + import_materials = [n for n in data_from.materials if n in mat_names] + print("import_materials", import_materials) + data_to.materials = import_materials + + mat_names -= set(import_materials) # Store already updated nodes + + new_materials = set(m for m in bpy.data.materials if m not in materials) + new_images = set(i for i in bpy.data.images if i not in images) + # + + for new_mat in new_materials: + new_mat_name = new_mat.library_weak_reference.id_name[2:] + local_mat = next((m for m in bpy.data.materials if m.name == new_mat_name and m != new_mat), None) + + if not local_mat: + print(f'No local material {new_mat_name}') + continue + + print(f'Merge material {local_mat.name} into {new_mat.name}') + + local_mat.user_remap(new_mat) + bpy.data.materials.remove(local_mat) + + if not new_mat.library: + new_mat.name = new_mat_name + new_mat.asset_clear() + + + return {'FINISHED'} + + + def draw(self, context): + layout = self.layout + layout.prop(self, "selection", expand=True) + + +bl_classes = ( + ASSETLIB_OT_update_materials, +) + + +def register(): + for bl_class in bl_classes: + bpy.utils.register_class(bl_class) + + +def unregister(): + for bl_class in reversed(bl_classes): + bpy.utils.unregister_class(bl_class) diff --git a/data_type/material/ui.py b/data_type/material/ui.py new file mode 100644 index 0000000..cd87ec2 --- /dev/null +++ b/data_type/material/ui.py @@ -0,0 +1,31 @@ +""" +This module contains blender UI elements in the node editor + +:author: Autour de Minuit +:maintainers: Christophe Seux +:date: 2024 +""" + +import bpy + + + +def draw_menu(self, context): + layout = self.layout + + row = layout.row(align=False) + layout.operator('assetlibrary.update_materials', text='Update Materials', icon='MATERIAL') + + +def register(): + # for c in classes: + # bpy.utils.register_class(c) + + bpy.types.ASSETLIB_MT_node_editor.append(draw_menu) + + +def unregister(): + # for c in reversed(classes): + # bpy.utils.unregister_class(c) + + bpy.types.ASSETLIB_MT_node_editor.remove(draw_menu) diff --git a/data_type/node/__init__.py b/data_type/node/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/data_type/node/operator.py b/data_type/node/operator.py new file mode 100644 index 0000000..c4866c4 --- /dev/null +++ b/data_type/node/operator.py @@ -0,0 +1,138 @@ + +from pathlib import Path +from datetime import datetime +import subprocess + +from ...core.bl_utils import get_bl_cmd +from ...core.lib_utils import get_asset_data, asset_library_map +from ...core.catalog import read_catalog +from ... import constants + +import bpy +from bpy.types import Operator +from bpy.props import StringProperty, EnumProperty + + +class ASSETLIB_OT_update_nodes(Operator): + bl_idname = 'assetlibrary.update_nodes' + bl_label = 'Update node' + bl_options = {"REGISTER", "UNDO"} + + selection : EnumProperty(items=[(s, s.title(), '') for s in ('ALL', 'SELECTED', 'CURRENT')], default="CURRENT", name='All Nodes') + + @classmethod + def poll(cls, context): + return context.space_data.edit_tree + + def invoke(self, context, event): + return context.window_manager.invoke_props_dialog(self) + + def execute(self, context): + asset_libraries = context.preferences.filepaths.asset_libraries + + ntree = context.space_data.edit_tree + ntree_name = ntree.name + new_ntree = None + + if self.selection == 'SELECTED': + nodes = [ n.node_tree for n in context.space_data.edit_tree.nodes + if n.type == "GROUP" and n.select] + elif self.selection == 'CURRENT': + active_node = context.space_data.edit_tree + nodes = [active_node] + else: + nodes = list(bpy.data.node_groups) + + library_map = asset_library_map() + + #node_groups = list(bpy.data.node_groups) + #images = list(bpy.data.images) + #materials = list(bpy.data.materials) + + with remap_datablock_duplicates(): + + for node in nodes: + blend_file = find_asset_source(library_map, 'node_groups', node.name) + link = bool(node.library) + + with bpy.data.libraries.load(str(blend_file), link=link) as (data_from, data_to): + data_to.node_groups = [node.name] + + for new_node_group in new_node_groups: + new_node_group_name = new_node_group.library_weak_reference.id_name[2:] + local_node_group = next((n for n in bpy.data.node_groups if n.name == new_node_group_name and n != new_node_group), None) + + if not local_node_group: + print(f'No local node_group {new_node_group_name}') + continue + + print(f'Merge node {local_node_group.name} into {new_node_group.name}') + + local_node_group.user_remap(new_node_group) + new_node_group.interface_update(context) + bpy.data.node_groups.remove(local_node_group) + + new_node_group.name = new_node_group_name + new_node_group.asset_clear() + + + node_names = set(n.name for n in nodes) + + for asset_library in asset_libraries: + library_path = Path(asset_library.path) + blend_files = [fp for fp in library_path.glob("**/*.blend") if fp.is_file()] + + node_groups = list(bpy.data.node_groups)# Storing original node_geoup to compare with imported + + link = (asset_library.import_method == 'LINK') + for blend_file in blend_files: + print(blend_file) + with bpy.data.libraries.load(str(blend_file), assets_only=True, link=link) as (data_from, data_to): + + import_node_groups = [n for n in data_from.node_groups if n in node_names] + print("import_node_groups", import_node_groups) + data_to.node_groups = import_node_groups + + node_names -= set(import_node_groups) # Store already updated nodes + + new_node_groups = set(n for n in bpy.data.node_groups if n not in node_groups) + + for new_node_group in new_node_groups: + new_node_group_name = new_node_group.library_weak_reference.id_name[2:] + local_node_group = next((n for n in bpy.data.node_groups if n.name == new_node_group_name and n != new_node_group), None) + + if not local_node_group: + print(f'No local node_group {new_node_group_name}') + continue + + print(f'Merge node {local_node_group.name} into {new_node_group.name}') + + local_node_group.user_remap(new_node_group) + new_node_group.interface_update(context) + bpy.data.node_groups.remove(local_node_group) + + new_node_group.name = new_node_group_name + new_node_group.asset_clear() + + + return {'FINISHED'} + + + def draw(self, context): + layout = self.layout + layout.prop(self, "selection", expand=True) + + +bl_classes = ( + ASSETLIB_OT_update_nodes, +) + + +def register(): + for bl_class in bl_classes: + bpy.utils.register_class(bl_class) + + +def unregister(): + for bl_class in reversed(bl_classes): + bpy.utils.unregister_class(bl_class) diff --git a/data_type/node/ui.py b/data_type/node/ui.py new file mode 100644 index 0000000..60ae1f2 --- /dev/null +++ b/data_type/node/ui.py @@ -0,0 +1,31 @@ +""" +This module contains blender UI elements in the node editor + +:author: Autour de Minuit +:maintainers: Christophe Seux +:date: 2024 +""" + +import bpy + + + +def draw_menu(self, context): + layout = self.layout + + row = layout.row(align=False) + layout.operator('assetlibrary.update_nodes', text='Update Node Group', icon='IMPORT') + + +def register(): + # for c in classes: + # bpy.utils.register_class(c) + + bpy.types.ASSETLIB_MT_node_editor.append(draw_menu) + + +def unregister(): + # for c in reversed(classes): + # bpy.utils.unregister_class(c) + + bpy.types.ASSETLIB_MT_node_editor.remove(draw_menu) diff --git a/data_type/pose/operators.py b/data_type/pose/operators.py index b5dbfd4..a11062f 100644 --- a/data_type/pose/operators.py +++ b/data_type/pose/operators.py @@ -35,7 +35,7 @@ from bpy.types import ( from bpy_extras import asset_utils from bpy_extras.io_utils import ExportHelper, ImportHelper -from asset_library.action.functions import ( +from asset_library.data_type.action.functions import ( get_marker, get_keyframes, ) diff --git a/operators.py b/operators.py index c133d60..2aa16c3 100644 --- a/operators.py +++ b/operators.py @@ -1,10 +1,20 @@ import importlib +from pathlib import Path import bpy +import subprocess +import gpu +from gpu_extras.batch import batch_for_shader +from mathutils import Vector +from math import sqrt -from bpy.types import Operator -from bpy.props import (BoolProperty, EnumProperty, StringProperty, IntProperty) -from .core.bl_utils import get_addon_prefs, unique_name +from bpy_extras.io_utils import ExportHelper +from bpy.types import Operator, PropertyGroup +from bpy.props import (BoolProperty, EnumProperty, StringProperty, IntProperty, CollectionProperty) +from .core.catalog import read_catalog +from .core.bl_utils import get_addon_prefs, unique_name, get_asset_type, get_bl_cmd, get_viewport +from .core.lib_utils import get_asset_full_path, get_asset_catalog_path, find_asset_data, clear_time_tag +from . import constants class ASSETLIB_OT_reload_addon(Operator): @@ -131,13 +141,559 @@ class ASSETLIB_OT_synchronize(Operator): subprocess.Popen(cmd) return {'FINISHED'} + +class ASSETLIB_OT_save_asset_preview(Operator): + bl_idname = "assetlibrary.save_asset_preview" + bl_options = {"REGISTER", "UNDO"} + bl_label = 'Save Asset Preview' + bl_description = 'Save Asset Preview' + + filepath: StringProperty( + name="File Path", + description="Filepath used for exporting the image", + subtype='FILE_PATH', + ) + check_existing: BoolProperty( + name="Check Existing", + description="Check and warn on overwriting existing files", + default=True, + options={'HIDDEN'}, + ) + + quality: IntProperty(subtype='PERCENTAGE', min=0, max=100, default=90, name='Quality') + + def execute(self, context): + prefs = get_addon_prefs() + + preview = None + + if context.asset.local_id: + preview = context.asset.local_id.preview + width, height = preview.image_size + pixels = [0] * width * height * 4 + preview.image_pixels_float.foreach_get(pixels) + + else: + asset_path = context.asset.full_library_path + asset_type, asset_name = Path(context.asset.full_path).parts[-2:] + asset_type = get_asset_type(asset_type) + + with bpy.data.temp_data(filepath=asset_path) as temp_data: + with temp_data.libraries.load(asset_path, assets_only=True, link=True) as (data_from, data_to): + setattr(data_to, asset_type, [asset_name]) + if assets := getattr(data_to, asset_type): + preview = assets[0].preview + width, height = preview.image_size + # Has to read pixel in the with statement for it to work + pixels = [0] * width * height * 4 + preview.image_pixels_float.foreach_get(pixels) + + if not preview: + self.report({'ERROR'}, 'Cannot retrieve preview') + return {"CANCELLED"} + + image = bpy.data.images.new('Asset Preview', width=width, height=height, alpha=True) + image.pixels.foreach_set(pixels) + try: + image.save(filepath=self.filepath, quality=self.quality) + except Exception as e: + print(e) + self.report({'ERROR'}, 'Cannot write preview') + return {"CANCELLED"} + + return {'FINISHED'} + + def invoke(self, context, event): + path = Path(context.asset.name) + if bpy.data.filepath: + path = Path(bpy.data.filepath, context.asset.name) + + self.filepath = str(path.with_suffix('.webp')) + + context.window_manager.fileselect_add(self) + return {'RUNNING_MODAL'} + + + + +class ASSETLIB_OT_make_custom_preview(Operator): + bl_idname = "assetlibrary.make_custom_preview" + bl_label = "Custom Preview" + bl_description = "Make a preview" + + #data_type : EnumProperty(name="Type", items=lambda s, c: constants.DATA_TYPE_ITEMS) + + def draw_border(self, context): + if not self.is_down: + return + + # 50% alpha, 2 pixel width line + shader = gpu.shader.from_builtin('UNIFORM_COLOR') + #gpu.state.line_width_set(1.0) + batch = batch_for_shader(shader, 'LINE_LOOP', {"pos": self.border}) + shader.uniform_float("color", (1.0, 0.0, 0.0, 1)) + batch.draw(shader) + + # restore opengl defaults + #gpu.state.line_width_set(1.0) + #gpu.state.blend_set('NONE') + + def grab_view3d(self, context): + width = int(self.release_window_pos.x - self.press_window_pos.x) + height = width#int(self.press_window_pos.y - self.release_window_pos.y) + x = int(self.press_window_pos.x) + y = int(self.press_window_pos.y - width) + + print(x, y, width, height) + + scene = context.scene + + fb = gpu.state.active_framebuffer_get() + buffer = fb.read_color(x, y, width, height, 4, 0, 'FLOAT') + + buffer.dimensions = width * height * 4 + + img = bpy.data.images.get('.Asset Preview') + if img: + bpy.data.images.remove(img) + img = bpy.data.images.new('.Asset Preview', width, height) + #img.scale(width, height) + img.pixels.foreach_set(buffer) + img.scale(256, 256) + + pixels = [0] * 256 * 256 * 4 + img.pixels.foreach_get(pixels) + + bpy.data.images.remove(img) + return pixels + + def modal(self, context, event): + context.area.tag_redraw() + + self.mouse_pos = Vector((event.mouse_region_x, event.mouse_region_y)) + + if event.type == 'LEFTMOUSE' and event.value == 'PRESS': + self.press_window_pos = Vector((event.mouse_x, event.mouse_y)) + self.press_pos = Vector((event.mouse_region_x, event.mouse_region_y)) + print('Start Border') + + self.is_down = True + + elif event.type == 'MOUSEMOVE' and self.is_down: + + + width = int(self.mouse_pos.x - self.press_pos.x) + X = (self.press_pos.x-1, self.mouse_pos.x +2) + Y = (self.press_pos.y+1, self.press_pos.y-width-2) + #print(self.mouse_pos, self.press_pos ) + + #X = sorted((self.press_pos.x, self.mouse_pos.x)) + #Y = sorted((self.press_pos.y, self.mouse_pos.y)) + #Constraint to square + #Y[0] = Y[1] - (X[1] - X[0]) + + self.border = [(X[0], Y[0]), (X[1], Y[0]), (X[1], Y[1]), (X[0], Y[1])] + + elif event.type == 'LEFTMOUSE' and event.value == 'RELEASE': + self.release_window_pos = Vector((event.mouse_x, event.mouse_y)) + bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW') + context.area.tag_redraw() + self.store_preview(context) + + return {'FINISHED'} + + elif event.type in {'RIGHTMOUSE', 'ESC'}: + bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW') + return {'CANCELLED'} + + return {'RUNNING_MODAL'} + + def store_preview(self, context): + asset = context.window_manager.asset_library.asset + pixels = self.grab_view3d(context) + asset.preview.image_size = 256, 256 + asset.preview.image_pixels_float.foreach_set(pixels) + + def invoke(self, context, event): + self.press_window_pos = Vector((0, 0)) + self.release_window_pos = Vector((0, 0)) + + self.press_pos = Vector((0, 0)) + + self.is_down = False + self.border = [] + + # Add the region OpenGL drawing callback + # draw in view space with 'POST_VIEW' and 'PRE_VIEW' + self._handle = bpy.types.SpaceView3D.draw_handler_add(self.draw_border, (context,), 'WINDOW', 'POST_PIXEL') + + area = get_viewport() + region = next(r for r in area.regions if r.type =="WINDOW") + with context.temp_override(area=area, space_data=area.spaces.active, region=region): + context.window_manager.modal_handler_add(self) + + return {'RUNNING_MODAL'} + #else: + # self.report({'WARNING'}, "View3D not found, cannot run operator") + # return {'CANCELLED'} + + +class ASSETLIB_OT_add_tag(Operator): + bl_idname = "assetlibrary.tag_add" + bl_options = {"REGISTER", "UNDO"} + bl_label = 'Add Tag' + bl_description = 'Add Tag' + + #data_type : EnumProperty(name="Type", items=lambda s, c: constants.DATA_TYPE_ITEMS) + + def execute(self, context): + asset = context.window_manager.asset_library.asset + + new_tag = asset.asset_data.tags.new(name='Tag') + index = list(asset.asset_data.tags).index(new_tag) + asset.asset_data.active_tag = index + + return {"FINISHED"} + + +class ASSETLIB_OT_remove_tag(Operator): + bl_idname = "assetlibrary.tag_remove" + bl_options = {"REGISTER", "UNDO"} + bl_label = 'Remove Tag' + bl_description = 'Remove Tag' + + #data_type : EnumProperty(name="Type", items=lambda s, c: constants.DATA_TYPE_ITEMS) + + def execute(self, context): + asset = context.window_manager.asset_library.asset + + if asset.asset_data.active_tag == -1: + return {"CANCELLED"} + + active_tag = asset.asset_data.tags[asset.asset_data.active_tag] + asset.asset_data.tags.remove(active_tag) + asset.asset_data.active_tag -=1 + + return {"FINISHED"} + + + +class ASSETLIB_OT_publish_asset(Operator): + bl_idname = "assetlibrary.publish_asset" + bl_options = {"REGISTER", "UNDO"} + bl_label = 'Publish Asset' + bl_description = 'Publish Asset' + + name : StringProperty(name='Name') + library : EnumProperty(name="Library", items=lambda s, c: constants.LIB_ITEMS) + #description : StringProperty(name='Description') + catalog : StringProperty(name='Catalog') + data_type : EnumProperty(name="Type", items=lambda s, c: constants.DATA_TYPE_ITEMS) + catalog_items : CollectionProperty(type=PropertyGroup) + + new_asset = False + is_asset = False + viewport = None + use_overlay = False + + # @classmethod + # def poll(self, context): + # return context.space_data.type == 'NODE_EDITOR' and context.space_data.edit_tree + + def invoke(self, context, event): + if self.data_type == 'NodeTree': + asset = context.space_data.edit_tree + elif self.data_type == 'Material': + asset = context.object.active_material + elif self.data_type == 'Object': + asset = context.object + + if asset.asset_data: + self.is_asset = True + else: + asset.asset_mark() + asset.preview_ensure() + asset.preview.image_size = 256, 256 + + self.viewport = get_viewport() + if self.viewport: + self.use_overlay = self.viewport.spaces.active.overlay.show_overlays + self.viewport.spaces.active.overlay.show_overlays = False + + bl_libs = context.preferences.filepaths.asset_libraries + constants.LIB_ITEMS[:] = [(lib.name, lib.name, "") for lib in bl_libs if lib.name] + + asset_type = get_asset_type(self.data_type) + asset_data = find_asset_data(asset.name, asset_type=asset_type, preview=True) + + for lib in bl_libs: + for catalog_item in read_catalog(lib.path): + c = self.catalog_items.add() + c.name = catalog_item.path + + self.name = asset.name + self.new_asset = True + if asset_data: + catalog = read_catalog(asset_data['library'].path) + if catalog_item := catalog.get(id=asset_data["catalog_id"]): + self.catalog = catalog_item.path + + self.new_asset = False + self.library = asset_data['library'].name + + if not self.is_asset: + if asset_data.get('preview_size'): + asset.preview.image_size = asset_data['preview_size'] + asset.preview.image_pixels_float.foreach_set(asset_data['preview_pixels']) + + asset.asset_data.description = asset_data['description'] + + for tag in asset_data['tags']: + asset.asset_data.tags.new(name=tag, skip_if_exists=True) + clear_time_tag(asset) + + #asset.preview_ensure() + context.window_manager.asset_library.asset = asset + + return context.window_manager.invoke_props_dialog(self) + + def check(self, context): + return True + + def cancel(self, context): + asset = context.window_manager.asset_library.asset + if self.viewport: + self.viewport.spaces.active.overlay.show_overlays = self.use_overlay + if not self.is_asset: + asset.asset_clear() + + def split_row(self, layout, name): + split = layout.split(factor=0.225) + split.alignment = 'RIGHT' + split.label(text=name) + return split + + def draw(self, context): + asset = context.window_manager.asset_library.asset + layout = self.layout + + col = layout.column() + col.use_property_split = True + col.use_property_decorate = False + + split = self.split_row(layout, "Name") + split.prop(self, "name", text='') + + split = self.split_row(layout, "Library") + split.prop(self, "library", text='') + + split = self.split_row(layout, "Catalog") + split.prop_search(self, "catalog", self, "catalog_items", results_are_suggestions=True, text='') + + split = self.split_row(layout, "Description") + split.prop(asset.asset_data, "description", text='') + + split = self.split_row(layout, "Tags") + row = split.row() + row.template_list("ASSETBROWSER_UL_metadata_tags", "asset_tags", asset.asset_data, "tags", + asset.asset_data, "active_tag", rows=3) + + col = row.column(align=True) + col.operator("assetlibrary.tag_add", icon='ADD', text="") + col.operator("assetlibrary.tag_remove", icon='REMOVE', text="") + + split = self.split_row(layout, "Preview") + row = split.row() + box = row.box() + box.template_icon(icon_value=asset.preview.icon_id, scale=5.0) + + col = row.column(align=False) + if self.viewport: + col.prop(self.viewport.spaces.active.overlay, 'show_overlays', icon="OVERLAY", text="") + col.operator("assetlibrary.make_custom_preview", icon='SCENE', text="") + #op.data_type = self.data_type + + def execute(self, context): + bl_libs = context.preferences.filepaths.asset_libraries + asset = context.window_manager.asset_library.asset + + publish_library = bl_libs[self.library] + asset_temp_blend = Path(bpy.app.tempdir, self.name).with_suffix('.blend') + + bpy.data.libraries.write(str(asset_temp_blend), {asset}, path_remap="ABSOLUTE") + + self.cancel(context) # To clear the asset mark and restore overlay + + asset_type = get_asset_type(self.data_type) + asset_full_path = Path(asset_temp_blend, asset_type, self.name) + + cmd = get_bl_cmd( + background=True, + factory_startup=True, + blendfile=constants.RESOURCES_DIR / 'asset_preview.blend', + script=constants.SCRIPTS_DIR / 'publish_library_assets.py', + library=publish_library.path, + assets=[asset_full_path.as_posix()], + catalogs=[self.catalog] + ) + + print(cmd) + subprocess.call(cmd) + + return {'FINISHED'} + + +class ASSETLIB_OT_publish_assets(Operator): + bl_idname = "assetlibrary.publish_assets" + bl_options = {"REGISTER", "UNDO"} + bl_label = 'Publish Assets' + bl_description = 'Publish Assets' + + library : EnumProperty(name="Library", items=lambda s, c: constants.LIB_ITEMS) + override : BoolProperty(default=True) + + # @classmethod + # def poll(self, context): + # return context.space_data.edit_tree + + def invoke(self, context, event): + bl_libs = context.preferences.filepaths.asset_libraries + constants.LIB_ITEMS[:] = [(lib.name, lib.name, "") for lib in bl_libs] + + return context.window_manager.invoke_props_dialog(self) + + def draw(self, context): + layout = self.layout + col = layout.column() + col.use_property_split = True + col.use_property_decorate = False + + layout.prop(self, "library") + layout.prop(self, "override") + + def execute(self, context): + bl_libs = context.preferences.filepaths.asset_libraries + publish_library = bl_libs[self.library] + preview_blend = constants.RESOURCES_DIR / 'asset_preview.blend' + + cmd = get_bl_cmd( + background=True, + factory_startup=True, + blendfile=preview_blend, + script=constants.SCRIPTS_DIR / 'publish_library_assets.py', + library=publish_library.path, + assets=[get_asset_full_path(a) for a in context.selected_assets], + catalogs=[get_asset_catalog_path(a) for a in context.selected_assets] + ) + + print(cmd) + subprocess.call(cmd) + + return {'FINISHED'} + + +class ASSETLIB_OT_update_assets(Operator): + bl_idname = 'assetlibrary.update_assets' + bl_label = 'Update node' + bl_options = {"REGISTER", "UNDO"} + + data_type : EnumProperty(name="Type", items=lambda s, c: constants.DATA_TYPE_ITEMS) + selection : EnumProperty( items=[(s, s.title(), '') for s in ('ALL', 'SELECTED', 'CURRENT')], + default="CURRENT", name='All Nodes') + + @classmethod + def poll(cls, context): + return context.space_data.edit_tree + + def invoke(self, context, event): + return context.window_manager.invoke_props_dialog(self) + + def execute(self, context): + asset_libraries = context.preferences.filepaths.asset_libraries + + if self.data_type == 'NodeTree': + assets = [context.space_data.edit_tree] + blend_data = bpy.data.node_groups + + if self.selection == 'SELECTED': + assets = [ n.node_tree for n in context.space_data.edit_tree.nodes + if n.type == "GROUP" and n.select] + elif self.selection == 'ALL': + assets = list(bpy.data.node_groups) + + elif self.data_type == 'Material': + asset = context.object.active_material + blend_data = bpy.data.materials + if self.selection == 'ALL': + assets = list(bpy.data.materials) + + elif self.data_type == 'Object': + return {"CANCELLED"} + + elif self.selection == 'CURRENT': + active_node = context.space_data.edit_tree + assets = [active_node] + else: + assets = list(bpy.data.node_groups) + + node_names = set(n.name for n in nodes) + + for asset_library in asset_libraries: + library_path = Path(asset_library.path) + blend_files = [fp for fp in library_path.glob("**/*.blend") if fp.is_file()] + + node_groups = list(bpy.data.node_groups)# Storing original node_geoup to compare with imported + + link = (asset_library.import_method == 'LINK') + for blend_file in blend_files: + print(blend_file) + with bpy.data.libraries.load(str(blend_file), assets_only=True, link=link) as (data_from, data_to): + + import_node_groups = [n for n in data_from.node_groups if n in node_names] + print("import_node_groups", import_node_groups) + data_to.node_groups = import_node_groups + + node_names -= set(import_node_groups) # Store already updated nodes + + new_node_groups = set(n for n in bpy.data.node_groups if n not in node_groups) + + for new_node_group in new_node_groups: + new_node_group_name = new_node_group.library_weak_reference.id_name[2:] + local_node_group = next((n for n in bpy.data.node_groups if n.name == new_node_group_name and n != new_node_group), None) + + if not local_node_group: + print(f'No local node_group {new_node_group_name}') + continue + + print(f'Merge node {local_node_group.name} into {new_node_group.name}') + + local_node_group.user_remap(new_node_group) + new_node_group.interface_update(context) + bpy.data.node_groups.remove(local_node_group) + + new_node_group.name = new_node_group_name + new_node_group.asset_clear() + + + return {'FINISHED'} + + + def draw(self, context): + layout = self.layout + layout.prop(self, "selection", expand=True) classes = ( ASSETLIB_OT_reload_addon, - ASSETLIB_OT_add_library, - ASSETLIB_OT_remove_library, - ASSETLIB_OT_synchronize + #ASSETLIB_OT_add_library, + #ASSETLIB_OT_remove_library, + #ASSETLIB_OT_synchronize, + ASSETLIB_OT_save_asset_preview, + ASSETLIB_OT_make_custom_preview, + ASSETLIB_OT_publish_asset, + ASSETLIB_OT_publish_assets, + ASSETLIB_OT_add_tag, + ASSETLIB_OT_remove_tag ) diff --git a/plugins/__init__.py b/plugins/__init__.py index a9a7056..e69de29 100644 --- a/plugins/__init__.py +++ b/plugins/__init__.py @@ -1,17 +0,0 @@ - -from asset_library.plugins import plugin -from asset_library.plugins import copy_folder -from asset_library.plugins import scan_folder - -if 'bpy' in locals(): - import importlib - - importlib.reload(plugin) - importlib.reload(copy_folder) - importlib.reload(scan_folder) - -import bpy - -LibraryPlugin = plugin.LibraryPlugin -CopyFolder = copy_folder.CopyFolder -ScanFolder = scan_folder.ScanFolder diff --git a/plugins/conform.py b/plugins/conform.py index 1669d55..73d1f32 100644 --- a/plugins/conform.py +++ b/plugins/conform.py @@ -4,9 +4,9 @@ Plugin for making an asset library of all blender file found in a folder """ -from .scan_folder import ScanFolder -from ..core.bl_utils import load_datablocks -from ..core.template import Template +from asset_library.plugins.scan_folder import ScanFolder +from asset_library.core.bl_utils import load_datablocks +from asset_library.core.template import Template import bpy from bpy.props import (StringProperty, IntProperty, BoolProperty) diff --git a/plugins/copy_folder.py b/plugins/copy_folder.py index 3562f35..cd016e6 100644 --- a/plugins/copy_folder.py +++ b/plugins/copy_folder.py @@ -8,8 +8,8 @@ from os.path import expandvars import bpy from bpy.props import StringProperty -from .library_plugin import LibraryPlugin -from ..core.file_utils import copy_dir +from asset_library.plugins.library_plugin import LibraryPlugin +from asset_library.core.file_utils import copy_dir diff --git a/plugins/kitsu.py b/plugins/kitsu.py index 94111a1..b9ca14c 100644 --- a/plugins/kitsu.py +++ b/plugins/kitsu.py @@ -17,9 +17,9 @@ import time import bpy from bpy.props import (StringProperty, IntProperty, BoolProperty) -from .library_plugin import LibraryPlugin -from ..core.template import Template -from ..core.file_utils import install_module +from asset_library.plugins.library_plugin import LibraryPlugin +from asset_library.core.template import Template +from asset_library.core.file_utils import install_module class Kitsu(LibraryPlugin): diff --git a/plugins/library_plugin.py b/plugins/library_plugin.py index 0e10c5e..89ea514 100644 --- a/plugins/library_plugin.py +++ b/plugins/library_plugin.py @@ -17,12 +17,12 @@ from bpy.types import PropertyGroup from bpy.props import StringProperty #from asset_library.common.functions import (norm_asset_datas,) -from ..core.bl_utils import get_addon_prefs, load_datablocks -from ..core.file_utils import read_file, write_file -from ..core.template import Template -from ..constants import (MODULE_DIR, RESOURCES_DIR) +from asset_library.core.bl_utils import get_addon_prefs, load_datablocks +from asset_library.core.file_utils import read_file, write_file +from asset_library.core.template import Template +from asset_library.constants import (MODULE_DIR, RESOURCES_DIR) -from ..data_type import (action, collection, file) +#from asset_library.data_type import (action, collection, file) #from asset_library.common.library_cache import LibraryCacheDiff diff --git a/plugins/poly_haven.py b/plugins/poly_haven.py index 908740d..e18e884 100644 --- a/plugins/poly_haven.py +++ b/plugins/poly_haven.py @@ -19,9 +19,9 @@ from pprint import pprint as pp import bpy from bpy.props import (StringProperty, IntProperty, BoolProperty, EnumProperty) -from .library_plugin import LibraryPlugin -from ..core.template import Template -from ..core.file_utils import install_module +from asset_library.plugins.library_plugin import LibraryPlugin +from asset_library.core.template import Template +from asset_library.core.file_utils import install_module REQ_HEADERS = requests.utils.default_headers() diff --git a/plugins/scan_folder.py b/plugins/scan_folder.py index babb60e..4549880 100644 --- a/plugins/scan_folder.py +++ b/plugins/scan_folder.py @@ -15,9 +15,9 @@ from itertools import groupby import bpy from bpy.props import (StringProperty, IntProperty, BoolProperty) -from .library_plugin import LibraryPlugin -from ..core.bl_utils import load_datablocks -from ..core.template import Template +from asset_library.plugins.library_plugin import LibraryPlugin +from asset_library.core.bl_utils import load_datablocks +from asset_library.core.template import Template diff --git a/preferences.py b/preferences.py index 0e55d5b..963bd3b 100644 --- a/preferences.py +++ b/preferences.py @@ -6,18 +6,19 @@ from bpy.props import (CollectionProperty, StringProperty) from . properties import AssetLibrary from . core.bl_utils import get_addon_prefs -from . core.asset_library_utils import update_library_path +from . core.lib_utils import update_library_path class AssetLibraryPrefs(AddonPreferences): bl_idname = __package__ + config_path : StringProperty(subtype="FILE_PATH") libraries : CollectionProperty(type=AssetLibrary) bundle_directory : StringProperty( name="Path", subtype='DIR_PATH', default='', - update=update_library_path + update=lambda s, c: update_library_path() ) def draw(self, context): @@ -26,6 +27,7 @@ class AssetLibraryPrefs(AddonPreferences): layout = self.layout col = layout.column(align=False) + col.prop(self, "config_path", text='Config') col.prop(self, "bundle_directory", text='Bundle Directory') col.separator() diff --git a/properties.py b/properties.py index fe50dc8..8d9bae7 100644 --- a/properties.py +++ b/properties.py @@ -1,18 +1,21 @@ import inspect +import os import bpy from bpy.types import (AddonPreferences, PropertyGroup) from bpy.props import (BoolProperty, StringProperty, CollectionProperty, EnumProperty, IntProperty, PointerProperty) -from .core.bl_utils import get_addon_prefs -from .core.file_utils import import_module_from_path, norm_str -from .core.asset_library_utils import update_library_path from .constants import PLUGINS, PLUGINS_DIR, PLUGINS_ITEMS +from .core.file_utils import import_module_from_path, norm_str + +from .core.bl_utils import get_addon_prefs +from .core.lib_utils import update_library_path def load_plugins(): + from .plugins.library_plugin import LibraryPlugin print('Asset Library: Load Library Plugins') plugin_files = list(PLUGINS_DIR.glob('*.py')) @@ -28,29 +31,31 @@ def load_plugins(): mod = import_module_from_path(plugin_file) for name, obj in inspect.getmembers(mod): - if not inspect.isclass(obj): + if not inspect.isclass(obj) or (obj is LibraryPlugin): continue - - if AssetLibrary not in obj.__mro__ or obj is AssetLibrary: + + if (LibraryPlugin not in obj.__mro__) or (obj in PLUGINS): continue try: print(f'Register Plugin {name}') bpy.utils.register_class(obj) setattr(Plugins, norm_str(obj.name), PointerProperty(type=obj)) - PLUGINS.append(obj) + PLUGINS[obj.name] = obj except Exception as e: print(f'Could not register plugin {name}') print(e) - plugin_items = [('NONE', 'None', '', 0)] - plugin_items += [(norm_str(p.name, format=str.upper), p.name, "", i+1) for i, a in enumerate(PLUGINS)] + plugins = sorted(PLUGINS.keys()) + plugin_items = [('none', 'None', '', 0)] + plugin_items += [(norm_str(p), p, "") for p in plugins] PLUGINS_ITEMS[:] = plugin_items return PLUGINS + class Plugins(PropertyGroup): """Container holding the registed library plugins""" def __iter__(self): @@ -60,15 +65,19 @@ class Plugins(PropertyGroup): class AssetLibrary(PropertyGroup): """Library item defining one library with his plugin and settings""" - name : StringProperty(name='Name', default='', update=update_library_path) + name : StringProperty(name='Name', default='', update=lambda s, c : update_library_path()) expand : BoolProperty(name='Expand', default=False) - use : BoolProperty(name='Use', default=True, update=update_library_path) - is_env : BoolProperty(default=False) - #path : StringProperty(subtype='DIR_PATH') + use : BoolProperty(name='Use', default=True, update=lambda s, c : update_library_path()) + is_user : BoolProperty(default=True) + path : StringProperty(subtype='DIR_PATH', update=lambda s, c : update_library_path()) plugins : PointerProperty(type=Plugins) plugin_name : EnumProperty(items=lambda s, c : PLUGINS_ITEMS) + @property + def plugin(self): + return getattr(self.plugins, self.plugin_name, None) + @property def index(self): prefs = get_addon_prefs() @@ -89,6 +98,30 @@ class AssetLibrary(PropertyGroup): layout.separator(factor=3) """ + def set_dict(self, data, obj=None): + """"Recursive method to set all attribute from a dict to this instance""" + + if obj is None: + obj = self + + # Make shure the input dict is not modidied + data = data.copy() + + for key, value in data.items(): + if isinstance(value, dict): + self.set_dict(value, obj=getattr(obj, key)) + + elif key in obj.bl_rna.properties.keys(): + + if isinstance(value, str): + value = os.path.expandvars(value) + value = os.path.expanduser(value) + + setattr(obj, key, value) + else: + print(f'Prop {key} of {obj} not exist') + + def draw(self, layout): prefs = get_addon_prefs() #col = layout.column(align=True) @@ -102,6 +135,10 @@ class AssetLibrary(PropertyGroup): #row.label(icon="ASSET_MANAGER") row.prop(self, 'name', text='') row.separator(factor=0.5) + sub = row.row() + sub.alignment = 'RIGHT' + sub.prop(self, 'plugin_name', text='') + row.separator(factor=0.5) op = row.operator("assetlibrary.synchronize", icon='UV_SYNC_SELECT', text='') op.name = self.name @@ -112,14 +149,26 @@ class AssetLibrary(PropertyGroup): #self.draw_operators(row) if self.expand: col = box.column(align=False) + col.use_property_split = True - - col.prop(self, "path") + col.prop(self, 'path', text='Path') + + if self.plugin: + col.separator() + self.plugin.draw_prefs(col) + + +class WindowManagerProperties(PropertyGroup): + """Library item defining one library with his plugin and settings""" + + asset : PointerProperty(type=bpy.types.ID) + classes = ( Plugins, AssetLibrary, + WindowManagerProperties ) @@ -127,8 +176,11 @@ def register(): for cls in classes: bpy.utils.register_class(cls) + bpy.types.WindowManager.asset_library = PointerProperty(type=WindowManagerProperties) load_plugins() def unregister(): for cls in reversed(classes): - bpy.utils.unregister_class(cls) \ No newline at end of file + bpy.utils.unregister_class(cls) + + del bpy.types.WindowManager.asset_library \ No newline at end of file diff --git a/resources/asset_preview.blend b/resources/asset_preview.blend new file mode 100644 index 0000000..cea51ab Binary files /dev/null and b/resources/asset_preview.blend differ diff --git a/resources/asset_preview.blend1 b/resources/asset_preview.blend1 new file mode 100644 index 0000000..a6cfa19 Binary files /dev/null and b/resources/asset_preview.blend1 differ diff --git a/scripts/publish_asset.py b/scripts/publish_asset.py new file mode 100644 index 0000000..1e2e55f --- /dev/null +++ b/scripts/publish_asset.py @@ -0,0 +1,42 @@ + +import argparse +import sys +import json +from pathlib import Path +import bpy + +from asset_library import constants +from asset_library.core.bl_utils import load_datablocks + + +def publish_asset(data_type, name): + bpy.app.use_userpref_skip_save_on_exit = True + + blend_file = bpy.data.filepath + preview_blend = constants.RESOURCES_DIR / 'asset_preview.blend' + col = load_datablocks(preview_blend, names='Preview', type='collections', link=False) + bpy.context.scene.collection.children.link(col) + + if data_type == 'node_groups': + ntree = bpy.data.node_groups[name] + mod = bpy.data.objects['Cube'].modifiers.new(ntree_name, 'NODES') + mod.node_group = ntree + + bpy.context.preferences.filepaths.save_version = 0 + bpy.ops.wm.save_mainfile(compress=True, exit=True) + #bpy.ops.wm.quit_blender() + + +if __name__ == '__main__' : + parser = argparse.ArgumentParser(description='build_collection_blends', + formatter_class=argparse.ArgumentDefaultsHelpFormatter) + + parser.add_argument('--data-type') + parser.add_argument('--datablock') + + if '--' in sys.argv : + index = sys.argv.index('--') + sys.argv = [sys.argv[index-1], *sys.argv[index+1:]] + + args = parser.parse_args() + publish_asset(**vars(args)) \ No newline at end of file diff --git a/scripts/publish_library_assets.py b/scripts/publish_library_assets.py new file mode 100644 index 0000000..4782267 --- /dev/null +++ b/scripts/publish_library_assets.py @@ -0,0 +1,96 @@ + +import argparse +import sys +import json +from pathlib import Path +import bpy +from datetime import datetime + +from asset_library import constants +from asset_library.core.bl_utils import load_datablocks +from asset_library.core.lib_utils import clear_time_tag, create_time_tag, version_file +from asset_library.core.catalog import Catalog + + +def get_all_datablocks(): + blend_datas = [ + bpy.data.collections, bpy.data.objects, + bpy.data.materials, bpy.data.node_groups, bpy.data.worlds] + + return [o for blend_data in blend_datas for o in blend_data] + + +def publish_library_assets(library, assets, catalogs): + bpy.app.use_userpref_skip_save_on_exit = True + bpy.context.preferences.filepaths.save_version = 0 + + for asset, catalog_path in zip(assets, catalogs): + asset_path, asset_type, asset_name = asset.rsplit('/', 2) + + #print(asset_path, asset_type, asset_name) + + asset = load_datablocks(asset_path, names=asset_name, type=asset_type, link=False) + + if not asset.asset_data: + asset.asset_mark() + + # clear asset_mark of all other assets + for datablock in get_all_datablocks(): + if datablock != asset and datablock.asset_data: + datablock.asset_clear() + + if asset_type == 'node_groups': + mod = bpy.data.objects['Cube'].modifiers.new(asset.name, 'NODES') + mod.node_group = asset + + elif asset_type == 'objects': + bpy.data.objects.remove(bpy.data.objects['Cube']) + bpy.data.collections['Preview'].objects.link(asset) + + elif asset_type == 'materials': + bpy.data.objects['Cube'].data.materials.append(asset) + + #catalog_path = asset.asset_data.catalog_simple_name.replace('-', '/') + asset_publish_path = Path(library, catalog_path, asset.name).with_suffix('.blend') + asset_publish_path.parent.mkdir(parents=True, exist_ok=True) + + # Assign or Create catalog + catalog = Catalog(library) + catalog.read() + + if catalog_item := catalog.get(path=catalog_path): + catalog_id = catalog_item.id + else: + catalog_id = catalog.add(catalog_path).id + catalog.write() + + asset.asset_data.catalog_id = catalog_id + + # Created time tag + clear_time_tag(asset) + create_time_tag(asset) + + version_file(asset_publish_path) + + bpy.ops.object.make_local(type='ALL') + bpy.ops.file.make_paths_relative() + bpy.ops.wm.save_as_mainfile(filepath=str(asset_publish_path), compress=True, copy=True, relative_remap=True) + bpy.ops.wm.revert_mainfile() + + bpy.ops.wm.quit_blender() + + +if __name__ == '__main__' : + parser = argparse.ArgumentParser(description='build_collection_blends', + formatter_class=argparse.ArgumentDefaultsHelpFormatter) + + parser.add_argument('--library') + parser.add_argument('--assets', nargs='+') + parser.add_argument('--catalogs', nargs='+') + + if '--' in sys.argv : + index = sys.argv.index('--') + sys.argv = [sys.argv[index-1], *sys.argv[index+1:]] + + args = parser.parse_args() + publish_library_assets(**vars(args)) \ No newline at end of file diff --git a/ui.py b/ui.py index 859dbdf..67eeccd 100644 --- a/ui.py +++ b/ui.py @@ -1,6 +1,25 @@ -from bpy.types import FILEBROWSER_HT_header, ASSETBROWSER_MT_editor_menus -from .core.asset_library_utils import get_active_library +import bpy +from bpy.types import Menu + +from .core.lib_utils import get_active_library + + +class ASSETLIB_MT_node_editor(Menu): + bl_label = "Asset" + + def draw(self, context): + layout = self.layout + op = layout.operator("assetlibrary.publish_asset", text='Publish Node Group', icon='NODETREE') + op.data_type = 'NodeTree' + + if context.space_data.tree_type == 'GeometryNodeTree': + op = layout.operator("assetlibrary.publish_asset", text='Publish Object', icon='OBJECT_DATA') + op.data_type = 'Object' + + elif context.space_data.tree_type == 'ShaderNodeTree': + op = layout.operator("assetlibrary.publish_asset", text='Publish Material', icon='MATERIAL') + op.data_type = 'Material' def draw_assetbrowser_header(self, context): @@ -51,16 +70,19 @@ def draw_assetbrowser_header(self, context): ).region_type = 'TOOL_PROPS' -def draw_assetbrowser_header(self, context): - if not get_active_library(): - return +def draw_assetbrowser_asset_menu(self, context): + layout = self.layout + layout.operator("assetlibrary.publish_assets", text='Publish Assets', icon='ASSET_MANAGER') - self.layout.separator() - box = self.layout.box() - row = box.row() - row.separator(factor=0.5) - row.label(text='Asset Library') - row.separator(factor=0.5) + # if not get_active_library(): + # return + + # self.layout.separator() + # box = self.layout.box() + # row = box.row() + # row.separator(factor=0.5) + # row.label(text='Asset Library') + # row.separator(factor=0.5) # classes = (, # # ASSETLIB_PT_pose_library_editing, @@ -69,18 +91,35 @@ def draw_assetbrowser_header(self, context): # # ASSETLIB_PT_libraries # ) -classes = [] +def draw_asset_preview_menu(self, context): + layout = self.layout + layout.operator("assetlibrary.save_asset_preview") + + +def draw_node_tree_menu(self, context): + layout = self.layout + row = layout.row(align=False) + row.menu('ASSETLIB_MT_node_editor') + + +bl_classes = ( + ASSETLIB_MT_node_editor,) def register() -> None: - for cls in classes: - bpy.utils.register_class(cls) + for bl_class in bl_classes: + bpy.utils.register_class(bl_class) - ASSETBROWSER_MT_editor_menus.append(draw_assetbrowser_header) + #bpy.types.ASSETBROWSER_MT_editor_menus.append(draw_assetbrowser_header) + bpy.types.ASSETBROWSER_MT_metadata_preview_menu.append(draw_asset_preview_menu) + bpy.types.NODE_MT_editor_menus.append(draw_node_tree_menu) + bpy.types.ASSETBROWSER_MT_asset.append(draw_assetbrowser_asset_menu) def unregister() -> None: - for cls in reversed(classes): - bpy.utils.unregister_class(cls) + for bl_class in reversed(bl_classes): + bpy.utils.unregister_class(bl_class) - ASSETBROWSER_MT_editor_menus.remove(draw_assetbrowser_header) + bpy.types.ASSETBROWSER_MT_editor_menus.remove(draw_assetbrowser_header) + bpy.types.NODE_MT_editor_menus.remove(draw_node_tree_menu) + bpy.types.ASSETBROWSER_MT_asset.remove(draw_assetbrowser_asset_menu) \ No newline at end of file