start refacto update

DEV
christophe.seux 2024-07-04 11:53:58 +02:00
parent a01b282f45
commit 4465148b22
35 changed files with 1700 additions and 188 deletions

View File

@ -16,14 +16,18 @@ bl_info = {
"category": "Import-Export", "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, operators,
properties, properties,
ui, ui,
preferences preferences,
data_type
) )
# Reload Modules from inside Blender # Reload Modules from inside Blender
@ -33,16 +37,33 @@ if "bpy" in locals():
for mod in modules: for mod in modules:
importlib.reload(mod) 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(): def register():
"""Register the addon Asset Library for Blender""" """Register the addon Asset Library for Blender"""
for mod in modules: for mod in bl_modules:
mod.register() mod.register()
bpy.app.timers.register(load_handler, first_interval=1)
def unregister(): def unregister():
"""Unregister the addon Asset Library for Blender""" """Unregister the addon Asset Library for Blender"""
for mod in reversed(modules): for mod in reversed(bl_modules):
mod.unregister() mod.unregister()

View File

@ -3,10 +3,18 @@ import bpy
DATA_TYPE_ITEMS = [ DATA_TYPE_ITEMS = [
("ACTION", "Action", "", "ACTION", 0), ("NodeTree", "Node Group", "", "NODETREE", 0),
("COLLECTION", "Collection", "", "OUTLINER_OB_GROUP_INSTANCE", 1), ("Material", "Material", "", "MATERIAL", 1),
("FILE", "File", "", "FILE", 2) ("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] DATA_TYPES = [i[0] for i in DATA_TYPE_ITEMS]
ICONS = {identifier: icon for identifier, name, description, icon, number 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' RESOURCES_DIR = MODULE_DIR / 'resources'
PLUGINS_DIR = MODULE_DIR / 'plugins' PLUGINS_DIR = MODULE_DIR / 'plugins'
PLUGINS = set() PLUGINS = {}
PLUGINS_ITEMS = [('NONE', 'None', '', 0)] 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' PREVIEW_ASSETS_SCRIPT = MODULE_DIR / 'common' / 'preview_assets.py'
#ADD_ASSET_DICT = {} #ADD_ASSET_DICT = {}

View File

@ -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

View File

@ -2,7 +2,7 @@
""" """
Generic Blender functions Generic Blender functions
""" """
import json
from pathlib import Path from pathlib import Path
from fnmatch import fnmatch from fnmatch import fnmatch
from typing import Any, List, Iterable, Optional, Tuple from typing import Any, List, Iterable, Optional, Tuple
@ -15,6 +15,8 @@ from bpy_extras import asset_utils
from os.path import abspath from os.path import abspath
import subprocess import subprocess
from .file_utils import norm_str
class attr_set(): class attr_set():
'''Receive a list of tuple [(data_path, "attribute" [, wanted value)] ] '''Receive a list of tuple [(data_path, "attribute" [, wanted value)] ]
@ -82,6 +84,9 @@ def get_viewport():
screen = bpy.context.screen screen = bpy.context.screen
areas = [a for a in screen.areas if a.type == 'VIEW_3D'] 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) areas.sort(key=lambda x : x.width*x.height)
return areas[-1] return areas[-1]
@ -216,7 +221,7 @@ def norm_arg(arg_name, format=str.lower, prefix='--', separator='-'):
return prefix + arg_name 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] cmd = [str(blender)] if blender else [bpy.app.binary_path]
if background: if background:
@ -228,6 +233,9 @@ def get_bl_cmd(blender=None, background=False, focus=True, blendfile=None, scrip
cmd += ['--python-use-system-env'] cmd += ['--python-use-system-env']
if factory_startup:
cmd += ['--factory-startup']
if blendfile: if blendfile:
cmd += [str(blendfile)] cmd += [str(blendfile)]
@ -347,7 +355,10 @@ def split_path(path) :
return bone_name, prop_name 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: def load_datablocks(src, names=None, type='objects', link=True, expr=None, assets_only=False) -> list:
@ -483,3 +494,85 @@ def get_object_libraries(ob):
filepaths.append(absolute_filepath) filepaths.append(absolute_filepath)
return filepaths 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

View File

@ -2,6 +2,14 @@
from pathlib import Path from pathlib import Path
import uuid import uuid
import bpy import bpy
from .file_utils import cache
@cache(1)
def read_catalog(library_path):
catalog = Catalog(library_path)
catalog.read()
return catalog
class CatalogItem: class CatalogItem:

View File

@ -12,9 +12,12 @@ from pathlib import Path
import importlib import importlib
import sys import sys
import shutil import shutil
from functools import wraps
from time import perf_counter
import contextlib import contextlib
@contextlib.contextmanager @contextlib.contextmanager
def cd(path): def cd(path):
"""Changes working directory and returns to previous on exit.""" """Changes working directory and returns to previous on exit."""
@ -25,6 +28,7 @@ def cd(path):
finally: finally:
os.chdir(prev_cwd) os.chdir(prev_cwd)
def install_module(module_name, package_name=None): def install_module(module_name, package_name=None):
'''Install a python module with pip or return it if already installed''' '''Install a python module with pip or return it if already installed'''
try: try:
@ -39,6 +43,7 @@ def install_module(module_name, package_name=None):
return module return module
def import_module_from_path(path): def import_module_from_path(path):
from importlib import util from importlib import util
@ -54,6 +59,7 @@ def import_module_from_path(path):
print(f'Cannot import file {path}') print(f'Cannot import file {path}')
print(e) print(e)
def norm_str(string, separator='_', format=str.lower, padding=0): def norm_str(string, separator='_', format=str.lower, padding=0):
string = str(string) string = str(string)
string = string.replace('_', ' ') string = string.replace('_', ' ')
@ -73,6 +79,7 @@ def norm_str(string, separator='_', format=str.lower, padding=0):
return string return string
def remove_version(filepath): def remove_version(filepath):
pattern = '_v[0-9]+\.' pattern = '_v[0-9]+\.'
search = re.search(pattern, filepath) search = re.search(pattern, filepath)
@ -82,12 +89,14 @@ def remove_version(filepath):
return Path(filepath).name return Path(filepath).name
def is_exclude(name, patterns) -> bool: def is_exclude(name, patterns) -> bool:
# from fnmatch import fnmatch # from fnmatch import fnmatch
if not isinstance(patterns, (list,tuple)) : if not isinstance(patterns, (list,tuple)) :
patterns = [patterns] patterns = [patterns]
return any([fnmatch(name, p) for p in 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: 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 '''Recursively get last(s) file(s) (when there is multiple versions) in passed directory
root -> str: Filepath of the folder to scan. 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) return sorted(files)
def copy_file(src, dst, only_new=False, only_recent=False): def copy_file(src, dst, only_new=False, only_recent=False):
if dst.exists(): if dst.exists():
if only_new: if only_new:
@ -153,6 +163,7 @@ def copy_file(src, dst, only_new=False, only_recent=False):
else: else:
subprocess.call(['cp', str(src), str(dst)]) subprocess.call(['cp', str(src), str(dst)])
def copy_dir(src, dst, only_new=False, only_recent=False, excludes=['.*'], includes=[]): def copy_dir(src, dst, only_new=False, only_recent=False, excludes=['.*'], includes=[]):
src, dst = Path(src), Path(dst) src, dst = Path(src), Path(dst)
@ -203,6 +214,7 @@ def open_file(filepath, select=False):
cmd += [str(filepath)] cmd += [str(filepath)]
subprocess.Popen(cmd) subprocess.Popen(cmd)
def open_blender_file(filepath=None): def open_blender_file(filepath=None):
filepath = filepath or bpy.data.filepath filepath = filepath or bpy.data.filepath
@ -217,6 +229,32 @@ def open_blender_file(filepath=None):
subprocess.Popen(cmd) 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): def read_file(path):
'''Read a file with an extension in (json, yaml, yml, txt)''' '''Read a file with an extension in (json, yaml, yml, txt)'''
@ -255,6 +293,7 @@ def read_file(path):
return data return data
def write_file(path, data, indent=4): def write_file(path, data, indent=4):
'''Read a file with an extension in (json, yaml, yml, text)''' '''Read a file with an extension in (json, yaml, yml, text)'''

319
core/lib_utils.py Normal file
View File

@ -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)

View File

@ -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()

View File

@ -1,26 +1,9 @@
from asset_library.action import ( from asset_library.data_type.action import (
gui,
keymaps, keymaps,
clear_asset,
concat_preview,
operators, 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 import bpy

View File

@ -4,9 +4,9 @@ import sys
from pathlib import Path from pathlib import Path
#sys.path.append(str(Path(__file__).parents[3])) #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.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 from asset_library.common.functions import read_catalog
import bpy import bpy

View File

@ -34,7 +34,7 @@ from asset_library.pose.pose_usage import(
flip_side_name flip_side_name
) )
from asset_library.action.functions import( from asset_library.data_type.action.functions import(
apply_anim, apply_anim,
append_action, append_action,
clean_action, clean_action,
@ -43,29 +43,16 @@ from asset_library.action.functions import(
conform_action conform_action
) )
from bpy.props import ( from bpy.props import (BoolProperty, CollectionProperty, EnumProperty,
BoolProperty, PointerProperty, StringProperty, IntProperty)
CollectionProperty,
EnumProperty,
PointerProperty,
StringProperty,
IntProperty
)
from bpy.types import ( from bpy.types import (Action, Context, Event, FileSelectEntry, Object,
Action, Operator, PropertyGroup)
Context,
Event,
FileSelectEntry,
Object,
Operator,
PropertyGroup,
)
from bpy_extras import asset_utils from bpy_extras import asset_utils
from bpy_extras.io_utils import ExportHelper, ImportHelper from bpy_extras.io_utils import ExportHelper, ImportHelper
from asset_library.action.functions import ( from asset_library.data_type.action.functions import (
is_pose, is_pose,
get_marker, get_marker,
get_keyframes, get_keyframes,

View File

@ -1,5 +1,5 @@
from asset_library.collection import ( from asset_library.data_type.collection import (
gui, gui,
operators, operators,
keymaps, keymaps,

View File

@ -1,5 +1,5 @@
from asset_library.file import ( from asset_library.data_type.file import (
operators, gui, keymaps) operators, gui, keymaps)
if 'bpy' in locals(): if 'bpy' in locals():

View File

View File

@ -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)

31
data_type/material/ui.py Normal file
View File

@ -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)

View File

138
data_type/node/operator.py Normal file
View File

@ -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)

31
data_type/node/ui.py Normal file
View File

@ -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)

View File

@ -35,7 +35,7 @@ from bpy.types import (
from bpy_extras import asset_utils from bpy_extras import asset_utils
from bpy_extras.io_utils import ExportHelper, ImportHelper 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_marker,
get_keyframes, get_keyframes,
) )

View File

@ -1,10 +1,20 @@
import importlib import importlib
from pathlib import Path
import bpy 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_extras.io_utils import ExportHelper
from bpy.props import (BoolProperty, EnumProperty, StringProperty, IntProperty) from bpy.types import Operator, PropertyGroup
from .core.bl_utils import get_addon_prefs, unique_name 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): class ASSETLIB_OT_reload_addon(Operator):
@ -133,11 +143,557 @@ class ASSETLIB_OT_synchronize(Operator):
return {'FINISHED'} 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 = ( classes = (
ASSETLIB_OT_reload_addon, ASSETLIB_OT_reload_addon,
ASSETLIB_OT_add_library, #ASSETLIB_OT_add_library,
ASSETLIB_OT_remove_library, #ASSETLIB_OT_remove_library,
ASSETLIB_OT_synchronize #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
) )

View File

@ -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

View File

@ -4,9 +4,9 @@ Plugin for making an asset library of all blender file found in a folder
""" """
from .scan_folder import ScanFolder from asset_library.plugins.scan_folder import ScanFolder
from ..core.bl_utils import load_datablocks from asset_library.core.bl_utils import load_datablocks
from ..core.template import Template from asset_library.core.template import Template
import bpy import bpy
from bpy.props import (StringProperty, IntProperty, BoolProperty) from bpy.props import (StringProperty, IntProperty, BoolProperty)

View File

@ -8,8 +8,8 @@ from os.path import expandvars
import bpy import bpy
from bpy.props import StringProperty from bpy.props import StringProperty
from .library_plugin import LibraryPlugin from asset_library.plugins.library_plugin import LibraryPlugin
from ..core.file_utils import copy_dir from asset_library.core.file_utils import copy_dir

View File

@ -17,9 +17,9 @@ import time
import bpy import bpy
from bpy.props import (StringProperty, IntProperty, BoolProperty) from bpy.props import (StringProperty, IntProperty, BoolProperty)
from .library_plugin import LibraryPlugin from asset_library.plugins.library_plugin import LibraryPlugin
from ..core.template import Template from asset_library.core.template import Template
from ..core.file_utils import install_module from asset_library.core.file_utils import install_module
class Kitsu(LibraryPlugin): class Kitsu(LibraryPlugin):

View File

@ -17,12 +17,12 @@ from bpy.types import PropertyGroup
from bpy.props import StringProperty from bpy.props import StringProperty
#from asset_library.common.functions import (norm_asset_datas,) #from asset_library.common.functions import (norm_asset_datas,)
from ..core.bl_utils import get_addon_prefs, load_datablocks from asset_library.core.bl_utils import get_addon_prefs, load_datablocks
from ..core.file_utils import read_file, write_file from asset_library.core.file_utils import read_file, write_file
from ..core.template import Template from asset_library.core.template import Template
from ..constants import (MODULE_DIR, RESOURCES_DIR) 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 #from asset_library.common.library_cache import LibraryCacheDiff

View File

@ -19,9 +19,9 @@ from pprint import pprint as pp
import bpy import bpy
from bpy.props import (StringProperty, IntProperty, BoolProperty, EnumProperty) from bpy.props import (StringProperty, IntProperty, BoolProperty, EnumProperty)
from .library_plugin import LibraryPlugin from asset_library.plugins.library_plugin import LibraryPlugin
from ..core.template import Template from asset_library.core.template import Template
from ..core.file_utils import install_module from asset_library.core.file_utils import install_module
REQ_HEADERS = requests.utils.default_headers() REQ_HEADERS = requests.utils.default_headers()

View File

@ -15,9 +15,9 @@ from itertools import groupby
import bpy import bpy
from bpy.props import (StringProperty, IntProperty, BoolProperty) from bpy.props import (StringProperty, IntProperty, BoolProperty)
from .library_plugin import LibraryPlugin from asset_library.plugins.library_plugin import LibraryPlugin
from ..core.bl_utils import load_datablocks from asset_library.core.bl_utils import load_datablocks
from ..core.template import Template from asset_library.core.template import Template

View File

@ -6,18 +6,19 @@ from bpy.props import (CollectionProperty, StringProperty)
from . properties import AssetLibrary from . properties import AssetLibrary
from . core.bl_utils import get_addon_prefs 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): class AssetLibraryPrefs(AddonPreferences):
bl_idname = __package__ bl_idname = __package__
config_path : StringProperty(subtype="FILE_PATH")
libraries : CollectionProperty(type=AssetLibrary) libraries : CollectionProperty(type=AssetLibrary)
bundle_directory : StringProperty( bundle_directory : StringProperty(
name="Path", name="Path",
subtype='DIR_PATH', subtype='DIR_PATH',
default='', default='',
update=update_library_path update=lambda s, c: update_library_path()
) )
def draw(self, context): def draw(self, context):
@ -26,6 +27,7 @@ class AssetLibraryPrefs(AddonPreferences):
layout = self.layout layout = self.layout
col = layout.column(align=False) col = layout.column(align=False)
col.prop(self, "config_path", text='Config')
col.prop(self, "bundle_directory", text='Bundle Directory') col.prop(self, "bundle_directory", text='Bundle Directory')
col.separator() col.separator()

View File

@ -1,18 +1,21 @@
import inspect import inspect
import os
import bpy import bpy
from bpy.types import (AddonPreferences, PropertyGroup) from bpy.types import (AddonPreferences, PropertyGroup)
from bpy.props import (BoolProperty, StringProperty, CollectionProperty, from bpy.props import (BoolProperty, StringProperty, CollectionProperty,
EnumProperty, IntProperty, PointerProperty) 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 .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(): def load_plugins():
from .plugins.library_plugin import LibraryPlugin
print('Asset Library: Load Library Plugins') print('Asset Library: Load Library Plugins')
plugin_files = list(PLUGINS_DIR.glob('*.py')) plugin_files = list(PLUGINS_DIR.glob('*.py'))
@ -28,29 +31,31 @@ def load_plugins():
mod = import_module_from_path(plugin_file) mod = import_module_from_path(plugin_file)
for name, obj in inspect.getmembers(mod): for name, obj in inspect.getmembers(mod):
if not inspect.isclass(obj): if not inspect.isclass(obj) or (obj is LibraryPlugin):
continue continue
if AssetLibrary not in obj.__mro__ or obj is AssetLibrary: if (LibraryPlugin not in obj.__mro__) or (obj in PLUGINS):
continue continue
try: try:
print(f'Register Plugin {name}') print(f'Register Plugin {name}')
bpy.utils.register_class(obj) bpy.utils.register_class(obj)
setattr(Plugins, norm_str(obj.name), PointerProperty(type=obj)) setattr(Plugins, norm_str(obj.name), PointerProperty(type=obj))
PLUGINS.append(obj) PLUGINS[obj.name] = obj
except Exception as e: except Exception as e:
print(f'Could not register plugin {name}') print(f'Could not register plugin {name}')
print(e) print(e)
plugin_items = [('NONE', 'None', '', 0)] plugins = sorted(PLUGINS.keys())
plugin_items += [(norm_str(p.name, format=str.upper), p.name, "", i+1) for i, a in enumerate(PLUGINS)] plugin_items = [('none', 'None', '', 0)]
plugin_items += [(norm_str(p), p, "") for p in plugins]
PLUGINS_ITEMS[:] = plugin_items PLUGINS_ITEMS[:] = plugin_items
return PLUGINS return PLUGINS
class Plugins(PropertyGroup): class Plugins(PropertyGroup):
"""Container holding the registed library plugins""" """Container holding the registed library plugins"""
def __iter__(self): def __iter__(self):
@ -60,15 +65,19 @@ class Plugins(PropertyGroup):
class AssetLibrary(PropertyGroup): class AssetLibrary(PropertyGroup):
"""Library item defining one library with his plugin and settings""" """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) expand : BoolProperty(name='Expand', default=False)
use : BoolProperty(name='Use', default=True, update=update_library_path) use : BoolProperty(name='Use', default=True, update=lambda s, c : update_library_path())
is_env : BoolProperty(default=False) is_user : BoolProperty(default=True)
#path : StringProperty(subtype='DIR_PATH') path : StringProperty(subtype='DIR_PATH', update=lambda s, c : update_library_path())
plugins : PointerProperty(type=Plugins) plugins : PointerProperty(type=Plugins)
plugin_name : EnumProperty(items=lambda s, c : PLUGINS_ITEMS) plugin_name : EnumProperty(items=lambda s, c : PLUGINS_ITEMS)
@property
def plugin(self):
return getattr(self.plugins, self.plugin_name, None)
@property @property
def index(self): def index(self):
prefs = get_addon_prefs() prefs = get_addon_prefs()
@ -89,6 +98,30 @@ class AssetLibrary(PropertyGroup):
layout.separator(factor=3) 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): def draw(self, layout):
prefs = get_addon_prefs() prefs = get_addon_prefs()
#col = layout.column(align=True) #col = layout.column(align=True)
@ -102,6 +135,10 @@ class AssetLibrary(PropertyGroup):
#row.label(icon="ASSET_MANAGER") #row.label(icon="ASSET_MANAGER")
row.prop(self, 'name', text='') row.prop(self, 'name', text='')
row.separator(factor=0.5) 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 = row.operator("assetlibrary.synchronize", icon='UV_SYNC_SELECT', text='')
op.name = self.name op.name = self.name
@ -112,14 +149,26 @@ class AssetLibrary(PropertyGroup):
#self.draw_operators(row) #self.draw_operators(row)
if self.expand: if self.expand:
col = box.column(align=False) col = box.column(align=False)
col.use_property_split = True
col.prop(self, "path") col.use_property_split = True
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 = ( classes = (
Plugins, Plugins,
AssetLibrary, AssetLibrary,
WindowManagerProperties
) )
@ -127,8 +176,11 @@ def register():
for cls in classes: for cls in classes:
bpy.utils.register_class(cls) bpy.utils.register_class(cls)
bpy.types.WindowManager.asset_library = PointerProperty(type=WindowManagerProperties)
load_plugins() load_plugins()
def unregister(): def unregister():
for cls in reversed(classes): for cls in reversed(classes):
bpy.utils.unregister_class(cls) bpy.utils.unregister_class(cls)
del bpy.types.WindowManager.asset_library

Binary file not shown.

Binary file not shown.

42
scripts/publish_asset.py Normal file
View File

@ -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))

View File

@ -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))

75
ui.py
View File

@ -1,6 +1,25 @@
from bpy.types import FILEBROWSER_HT_header, ASSETBROWSER_MT_editor_menus import bpy
from .core.asset_library_utils import get_active_library 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): def draw_assetbrowser_header(self, context):
@ -51,16 +70,19 @@ def draw_assetbrowser_header(self, context):
).region_type = 'TOOL_PROPS' ).region_type = 'TOOL_PROPS'
def draw_assetbrowser_header(self, context): def draw_assetbrowser_asset_menu(self, context):
if not get_active_library(): layout = self.layout
return layout.operator("assetlibrary.publish_assets", text='Publish Assets', icon='ASSET_MANAGER')
self.layout.separator() # if not get_active_library():
box = self.layout.box() # return
row = box.row()
row.separator(factor=0.5) # self.layout.separator()
row.label(text='Asset Library') # box = self.layout.box()
row.separator(factor=0.5) # row = box.row()
# row.separator(factor=0.5)
# row.label(text='Asset Library')
# row.separator(factor=0.5)
# classes = (, # classes = (,
# # ASSETLIB_PT_pose_library_editing, # # ASSETLIB_PT_pose_library_editing,
@ -69,18 +91,35 @@ def draw_assetbrowser_header(self, context):
# # ASSETLIB_PT_libraries # # 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: def register() -> None:
for cls in classes: for bl_class in bl_classes:
bpy.utils.register_class(cls) 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: def unregister() -> None:
for cls in reversed(classes): for bl_class in reversed(bl_classes):
bpy.utils.unregister_class(cls) 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)