Compare commits

...

2 Commits
master ... DEV

Author SHA1 Message Date
christophe.seux 4465148b22 start refacto update 2024-07-04 11:53:58 +02:00
christophe.seux a01b282f45 start refacto 2024-05-27 17:22:45 +02:00
72 changed files with 2046 additions and 3249 deletions

View File

@ -8,96 +8,62 @@ Extending features of the Asset Browser for a studio use.
bl_info = {
"name": "Asset Library",
"description": "Asset Library based on the Asset Browser.",
"author": "Sybren A. Stüvel, Clement Ducarteron, Christophe Seux, Samuel Bernou",
"author": "Christophe Seux",
"version": (2, 0),
"blender": (3, 3, 0),
"blender": (4, 0, 2),
"warning": "In development, things may change",
"location": "Asset Browser -> Animations, and 3D Viewport -> Animation panel",
"category": "Animation",
"location": "Asset Browser",
"category": "Import-Export",
}
#from typing import List, Tuple
import sys
from . import operators, properties, ui, preferences, data_type
from .core.lib_utils import load_libraries, update_library_path
from asset_library import pose
from asset_library import action
from asset_library import collection
from asset_library import file
from asset_library import (gui, keymaps, preferences, operators)
from asset_library import constants
#from asset_library.common.library_type import LibraryType
from asset_library.common.bl_utils import get_addon_prefs
from asset_library.common.functions import set_env_libraries
from asset_library.common.template import Template
import re
if 'bpy' in locals():
print("Reload Addon Asset Library")
import importlib
importlib.reload(constants)
importlib.reload(gui)
importlib.reload(keymaps)
importlib.reload(preferences)
importlib.reload(operators)
importlib.reload(constants)
importlib.reload(action)
importlib.reload(file)
importlib.reload(collection)
import bpy
import os
#addon_keymaps: List[Tuple[bpy.types.KeyMap, bpy.types.KeyMapItem]] = []
bl_modules = (
operators,
pose,
action,
collection,
file,
keymaps,
gui,
preferences
properties,
ui,
preferences,
data_type
)
# Reload Modules from inside Blender
if "bpy" in locals():
import importlib
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)
set_env_libraries()
bpy.ops.assetlib.set_paths(all=True)
if not bpy.app.background:
bpy.ops.assetlib.bundle(blocking=False, mode='AUTO_BUNDLE')
#if not bpy.app.background:
# bpy.ops.assetlib.bundle(blocking=False, mode='AUTO_BUNDLE')
def register() -> None:
def register():
"""Register the addon Asset Library for Blender"""
for mod in bl_modules:
mod.register()
for m in bl_modules:
m.register()
#prefs = get_addon_prefs()
bpy.app.timers.register(load_handler, first_interval=1)
def unregister() -> None:
prefs = get_addon_prefs()
bpy.utils.previews.remove(prefs.previews)
def unregister():
"""Unregister the addon Asset Library for Blender"""
for m in reversed(bl_modules):
m.unregister()
for mod in reversed(bl_modules):
mod.unregister()

View File

@ -1,33 +0,0 @@
from asset_library.action import (
gui,
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
def register():
operators.register()
keymaps.register()
def unregister():
operators.unregister()
keymaps.unregister()

View File

@ -1,12 +0,0 @@
from bpy.types import PropertyGroup
class Adapter(PropertyGroup):
#def __init__(self):
name = "Base Adapter"
#library = None
def to_dict(self):
return {p: getattr(self, p) for p in self.bl_rna.properties.keys() if p !='rna_type'}

View File

@ -1,18 +0,0 @@
from asset_library.common import file_utils
from asset_library.common import functions
from asset_library.common import synchronize
from asset_library.common import template
from asset_library.common import catalog
if 'bpy' in locals():
import importlib
importlib.reload(file_utils)
importlib.reload(functions)
importlib.reload(synchronize)
importlib.reload(template)
importlib.reload(catalog)
import bpy

View File

@ -1,467 +0,0 @@
# SPDX-License-Identifier: GPL-2.0-or-later
"""
Function relative to the asset browser addon
"""
from pathlib import Path
import json
import os
import re
import time
#from asset_library.constants import ASSETLIB_FILENAME
import inspect
from asset_library.common.file_utils import read_file
from asset_library.common.bl_utils import get_addon_prefs
import uuid
import bpy
def command(func):
'''Decorator to be used from printed functions argument and run time'''
func_name = func.__name__.replace('_', ' ').title()
def _command(*args, **kargs):
bound = inspect.signature(func).bind(*args, **kargs)
bound.apply_defaults()
args_str = ', '.join([f'{k}={v}' for k, v in bound.arguments.items()])
print(f'\n[>-] {func_name} ({args_str}) --- Start ---')
t0 = time.time()
result = func(*args, **kargs)
print(f'[>-] {func_name} --- Finished (total time : {time.time() - t0:.2f}s) ---')
return result
return _command
def asset_warning_callback(self, context):
"""Callback function to display a warning message when ading or modifying an asset"""
self.warning = ''
if not self.name:
self.warning = 'You need to specify a name'
return
if not self.catalog:
self.warning = 'You need to specify a catalog'
return
lib = get_active_library()
action_path = lib.library_type.get_asset_relative_path(self.name, self.catalog)
self.path = action_path.as_posix()
if lib.merge_libraries:
prefs = get_addon_prefs()
lib = prefs.libraries[lib.store_library]
if not lib.library_type.get_asset_path(self.name, self.catalog).parents[1].exists():
self.warning = 'A new folder will be created'
def get_active_library():
'''Get the pref library properties from the active library of the asset browser'''
prefs = get_addon_prefs()
asset_lib_ref = bpy.context.space_data.params.asset_library_ref
#Check for merged library
for l in prefs.libraries:
if l.library_name == asset_lib_ref:
return l
def get_active_catalog():
'''Get the active catalog path'''
lib = get_active_library()
cat_data = lib.library_type.read_catalog()
cat_data = {v['id']:k for k,v in cat_data.items()}
cat_id = bpy.context.space_data.params.catalog_id
if cat_id in cat_data:
return cat_data[cat_id]
return ''
"""
def norm_asset_datas(asset_file_datas):
''' Return a new flat list of asset data
the filepath keys are merge with the assets keys'''
asset_datas = []
for asset_file_data in asset_file_datas:
asset_file_data = asset_file_data.copy()
if 'assets' in asset_file_data:
assets = asset_file_data.pop('assets')
for asset_data in assets:
asset_datas.append({**asset_file_data, **asset_data})
else:
asset_datas.append(asset_file_data)
return asset_datas
def cache_diff(cache, new_cache):
'''Compare and return the difference between two asset datas list'''
#TODO use an id to be able to tell modified asset if renamed
#cache = {a.get('id', a['name']) : a for a in norm_asset_datas(cache)}
#new_cache = {a.get('id', a['name']) : a for a in norm_asset_datas(new_cache)}
cache = {f"{a['filepath']}/{a['name']}": a for a in norm_asset_datas(cache)}
new_cache = {f"{a['filepath']}/{a['name']}" : a for a in norm_asset_datas(new_cache)}
assets_added = [v for k, v in new_cache.items() if k not in cache]
assets_removed = [v for k, v in cache.items() if k not in new_cache]
assets_modified = [v for k, v in cache.items() if v not in assets_removed and v!= new_cache[k]]
if assets_added:
print(f'{len(assets_added)} Assets Added \n{tuple(a["name"] for a in assets_added[:10])}\n')
if assets_removed:
print(f'{len(assets_removed)} Assets Removed \n{tuple(a["name"] for a in assets_removed[:10])}\n')
if assets_modified:
print(f'{len(assets_modified)} Assets Modified \n{tuple(a["name"] for a in assets_modified[:10])}\n')
assets_added = [dict(a, operation='ADD') for a in assets_added]
assets_removed = [dict(a, operation='REMOVE') for a in assets_removed]
assets_modified = [dict(a, operation='MODIFY') for a in assets_modified]
assets_diff = assets_added + assets_removed + assets_modified
if not assets_diff:
print('No change in the library')
return assets_diff
def clean_default_lib():
prefs = bpy.context.preferences
if not prefs.filepaths.asset_libraries:
print('[>-] No Asset Libraries Filepaths Setted.')
return
lib, lib_id = get_lib_id(
library_name='User Library',
asset_libraries=prefs.filepaths.asset_libraries
)
if lib:
bpy.ops.preferences.asset_library_remove(index=lib_id)
def get_asset_source(replace_local=False):
sp = bpy.context.space_data
prefs = bpy.context.preferences.addons[__package__].preferences
asset_file_handle = bpy.context.asset_file_handle
if asset_file_handle is None:
return None
if asset_file_handle.local_id:
publish_path = os.path.expandvars(scn.actionlib.get('publish_path'))
if not publish_path:
print('[>.] No \'Publish Dir\' found. Publish file first.' )
return None
return Path(publish_path)
asset_library_ref = bpy.context.asset_library_ref
source_path = bpy.types.AssetHandle.get_full_library_path(asset_file_handle, asset_library_ref)
if replace_local:
if 'custom' in sp.params.asset_library_ref.lower():
actionlib_path = prefs.action.custom_path
actionlib_path_local = prefs.action.custom_path_local
else:
actionlib_path = prefs.action.path
actionlib_path_local = prefs.action.path_local
source_path = re.sub(actionlib_dir_local, actionlib_dir, source_path)
return source_path
"""
'''
def get_catalog_path(filepath=None):
filepath = filepath or bpy.data.filepath
filepath = Path(filepath)
if filepath.is_file():
filepath = filepath.parent
filepath.mkdir(parents=True, exist_ok=True)
catalog = filepath / 'blender_assets.cats.txt'
if not catalog.exists():
catalog.touch(exist_ok=False)
return catalog
'''
# def read_catalog(path, key='path'):
# cat_data = {}
# supported_keys = ('path', 'id', 'name')
# if key not in supported_keys:
# raise Exception(f'Not supported key: {key} for read catalog, supported keys are {supported_keys}')
# for line in Path(path).read_text(encoding="utf-8").split('\n'):
# if line.startswith(('VERSION', '#')) or not line:
# continue
# cat_id, cat_path, cat_name = line.split(':')
# if key == 'id':
# cat_data[cat_id] = {'path':cat_path, 'name':cat_name}
# elif key == 'path':
# cat_data[cat_path] = {'id':cat_id, 'name':cat_name}
# elif key =='name':
# cat_data[cat_name] = {'id':cat_id, 'path':cat_path}
# return cat_data
"""
def read_catalog(path):
cat_data = {}
for line in Path(path).read_text(encoding="utf-8").split('\n'):
if line.startswith(('VERSION', '#')) or not line:
continue
cat_id, cat_path, cat_name = line.split(':')
cat_data[cat_path] = {'id':cat_id, 'name':cat_name}
return cat_data
def write_catalog(path, data):
lines = ['VERSION 1', '']
# Add missing parents catalog
norm_data = {}
for cat_path, cat_data in data.items():
norm_data[cat_path] = cat_data
for p in Path(cat_path).parents[:-1]:
if p in data or p in norm_data:
continue
norm_data[p.as_posix()] = {'id': str(uuid.uuid4()), 'name': '-'.join(p.parts)}
for cat_path, cat_data in sorted(norm_data.items()):
cat_name = cat_data['name'].replace('/', '-')
lines.append(f"{cat_data['id']}:{cat_path}:{cat_name}")
print(f'Catalog writen at: {path}')
Path(path).write_text('\n'.join(lines), encoding="utf-8")
def create_catalog_file(json_path : str|Path, keep_existing_category : bool = True):
'''create asset catalog file from json
if catalog already exists, keep existing catalog uid'''
json_path = Path(json_path)
# if not json.exists(): return
assert json_path.exists(), 'Json not exists !'
category_datas = json.loads(json_path.read_text(encoding="utf-8"))
catalog_path = json_path.parent / 'blender_assets.cats.txt'
catalog_data = {}
if catalog_path.exists():
catalog_data = read_catalog(catalog_path)
## retrun a format catalog_data[path] = {'id':id, 'name':name}
## note: 'path' in catalog is 'name' in category_datas
catalog_lines = ['VERSION 1', '']
## keep existing
for c in category_datas:
# keep same catalog line for existing category keys
if keep_existing_category and catalog_data.get(c['name']):
print(c['name'], 'category exists')
cat = catalog_data[c['name']] #get
catalog_lines.append(f"{cat['id']}:{c['name']}:{cat['name']}")
else:
print(c['name'], 'new category')
# add new category
catalog_lines.append(f"{c['id']}:{c['name']}:{c['name'].replace('/', '-')}")
## keep category that are non-existing in json ?
if keep_existing_category:
for k in catalog_data.keys():
if next((c['name'] for c in category_datas if c['name'] == k), None):
continue
print(k, 'category not existing in json')
cat = catalog_data[k]
# rebuild existing line
catalog_lines.append(f"{cat['id']}:{k}:{cat['name']}")
## write_text overwrite the file
catalog_path.write_text('\n'.join(catalog_lines), encoding="utf-8")
print(f'Catalog saved at: {catalog_path}')
return
"""
def clear_env_libraries():
print('clear_env_libraries')
prefs = get_addon_prefs()
asset_libraries = bpy.context.preferences.filepaths.asset_libraries
for env_lib in prefs.env_libraries:
name = env_lib.get('asset_library')
if not name:
continue
asset_lib = asset_libraries.get(name)
if not asset_lib:
continue
index = list(asset_libraries).index(asset_lib)
bpy.ops.preferences.asset_library_remove(index=index)
prefs.env_libraries.clear()
'''
env_libs = get_env_libraries()
paths = [Path(l['path']).resolve().as_posix() for n, l in env_libs.items()]
for i, l in reversed(enumerate(libs)):
lib_path = Path(l.path).resolve().as_posix()
if (l.name in env_libs or lib_path in paths):
libs.remove(i)
'''
def set_env_libraries(path=None) -> list:
'''Read the environments variables and create the libraries'''
#from asset_library.prefs import AssetLibraryOptions
prefs = get_addon_prefs()
path = path or prefs.config_directory
#print('Read', path)
library_data = read_file(path)
clear_env_libraries()
if not library_data:
return
libs = []
for lib_info in library_data:
lib = prefs.env_libraries.add()
lib.set_dict(lib_info)
libs.append(lib)
return libs
'''
def get_env_libraries():
env_libraries = {}
for k, v in os.environ.items():
if not re.findall('ASSET_LIBRARY_[0-9]', k):
continue
lib_infos = v.split(os.pathsep)
if len(lib_infos) == 5:
name, data_type, tpl, src_path, bdl_path = lib_infos
elif len(lib_infos) == 4:
name, data_type, tpl, src_path = lib_infos
bdl_path = ''
else:
print(f'Wrong env key {k}', lib_infos)
continue
source_type = 'TEMPLATE'
if tpl.lower().endswith(('.json', '.yml', 'yaml')):
source_type = 'DATA_FILE'
env_libraries[name] = {
'data_type': data_type,
'source_directory': src_path,
'bundle_directory': bdl_path,
'source_type': source_type,
'template': tpl,
}
return env_libraries
'''
def resync_lib(name, waiting_time):
bpy.app.timers.register(
lambda: bpy.ops.assetlib.synchronize(only_recent=True, name=name),
first_interval=waiting_time
)
'''
def set_assetlib_paths():
prefs = bpy.context.preferences
assetlib_name = 'Assets'
assetlib = prefs.filepaths.asset_libraries.get(assetlib_name)
if not assetlib:
bpy.ops.preferences.asset_library_add(directory=str(assetlib_path))
assetlib = prefs.filepaths.asset_libraries[-1]
assetlib.name = assetlib_name
assetlib.path = str(actionlib_dir)
def set_actionlib_paths():
prefs = bpy.context.preferences
actionlib_name = 'Action Library'
actionlib_custom_name = 'Action Library Custom'
actionlib = prefs.filepaths.asset_libraries.get(actionlib_name)
if not assetlib:
bpy.ops.preferences.asset_library_add(directory=str(assetlib_path))
assetlib = prefs.filepaths.asset_libraries[-1]
assetlib.name = assetlib_name
actionlib_dir = get_actionlib_dir(custom=custom)
local_actionlib_dir = get_actionlib_dir(local=True, custom=custom)
if local_actionlib_dir:
actionlib_dir = local_actionlib_dir
if actionlib_name not in prefs.filepaths.asset_libraries:
bpy.ops.preferences.asset_library_add(directory=str(actionlib_dir))
#lib, lib_id = get_lib_id(
# library_path=actionlib_dir,
# asset_libraries=prefs.filepaths.asset_libraries
#)
#if not lib:
# print(f'Cannot set dir for {actionlib_name}')
# return
prefs.filepaths.asset_libraries[lib_id].name = actionlib_name
#prefs.filepaths.asset_libraries[lib_id].path = str(actionlib_dir)
'''

View File

@ -1,381 +0,0 @@
import bpy
from pathlib import Path
from asset_library.common.file_utils import read_file, write_file
from copy import deepcopy
import time
from itertools import groupby
class AssetCache:
def __init__(self, file_cache, data=None):
self.file_cache = file_cache
self.catalog = None
self.author = None
self.description = None
self.tags = None
self.type = None
self.name = None
self._metadata = None
if data:
self.set_data(data)
@property
def filepath(self):
return self.file_cache.filepath
@property
def library_id(self):
return self.file_cache.library_id
@property
def metadata(self):
metadata = {
'.library_id': self.library_id,
'.filepath': self.filepath
}
metadata.update(self._metadata)
return metadata
@property
def norm_name(self):
return self.name.replace(' ', '_').lower()
def unique_name(self):
return (self.filepath / self.name).as_posix()
def set_data(self, data):
catalog = data['catalog']
if isinstance(catalog, (list, tuple)):
catalog = '/'.join(catalog)
self.catalog = catalog
self.author = data.get('author', '')
self.description = data.get('description', '')
self.tags = data.get('tags', [])
self.type = data.get('type')
self.name = data['name']
self._metadata = data.get('metadata', {})
def to_dict(self):
return dict(
catalog=self.catalog,
author=self.author,
metadata=self.metadata,
description=self.description,
tags=self.tags,
type=self.type,
name=self.name
)
def __repr__(self):
return f'AssetCache(name={self.name}, catalog={self.catalog})'
def __eq__(self, other):
return self.to_dict() == other.to_dict()
class AssetsCache:
def __init__(self, file_cache):
self.file_cache = file_cache
self._data = []
def add(self, asset_cache_data, **kargs):
asset_cache = AssetCache(self.file_cache, {**asset_cache_data, **kargs})
self._data.append(asset_cache)
return asset_cache
def remove(self, asset_cache):
if isinstance(asset_cache, str):
asset_cache = self.get(asset_cache)
def __iter__(self):
return self._data.__iter__()
def __getitem__(self, key):
if isinstance(key, str):
return self.to_dict()[key]
else:
return self._data[key]
def to_dict(self):
return {a.name: a for a in self}
def get(self, name):
return next((a for a in self if a.name == name), None)
def __repr__(self):
return f'AssetsCache({list(self)})'
class FileCache:
def __init__(self, library_cache, data=None):
self.library_cache = library_cache
self.filepath = None
self.modified = None
self.assets = AssetsCache(self)
if data:
self.set_data(data)
@property
def library_id(self):
return self.library_cache.library_id
def set_data(self, data):
if 'filepath' in data:
self.filepath = Path(data['filepath'])
self.modified = data.get('modified', time.time_ns())
if data.get('type') == 'FILE':
self.assets.add(data)
for asset_cache_data in data.get('assets', []):
self.assets.add(asset_cache_data)
def to_dict(self):
return dict(
filepath=self.filepath.as_posix(),
modified=self.modified,
library_id=self.library_id,
assets=[asset_cache.to_dict() for asset_cache in self]
)
def __iter__(self):
return self.assets.__iter__()
def __getitem__(self, key):
return self._data[key]
def __repr__(self):
return f'FileCache(filepath={self.filepath})'
class AssetCacheDiff:
def __init__(self, library_cache, asset_cache, operation):
self.library_cache = library_cache
#self.filepath = data['filepath']
self.operation = operation
self.asset_cache = asset_cache
class LibraryCacheDiff:
def __init__(self, old_cache=None, new_cache=None, filepath=None):
self.filepath = filepath
self._data = []
self.compare(old_cache, new_cache)
def add(self, asset_cache_datas, operation):
if not isinstance(asset_cache_datas, (list, tuple)):
asset_cache_datas = [asset_cache_datas]
new_asset_diffs = []
for cache_data in asset_cache_datas:
new_asset_diffs.append(AssetCacheDiff(self, cache_data, operation))
self._data += new_asset_diffs
return new_asset_diffs
def compare(self, old_cache, new_cache):
if old_cache is None or new_cache is None:
print('Cannot Compare cache with None')
cache_dict = {a.unique_name : a for a in old_cache.asset_caches}
new_cache_dict = {a.unique_name : a for a in new_cache.asset_caches}
assets_added = self.add([v for k, v in new_cache_dict.items() if k not in cache_dict], 'ADD')
assets_removed = self.add([v for k, v in cache_dict.items() if k not in new_cache_dict], 'REMOVED')
assets_modified = self.add([v for k, v in cache_dict.items() if v not in assets_removed and v!= new_cache_dict[k]], 'MODIFIED')
if assets_added:
print(f'{len(assets_added)} Assets Added \n{tuple(a.name for a in assets_added[:10])}...\n')
if assets_removed:
print(f'{len(assets_removed)} Assets Removed \n{tuple(a.name for a in assets_removed[:10])}...\n')
if assets_modified:
print(f'{len(assets_modified)} Assets Modified \n{tuple(a.name for a in assets_modified[:10])}...\n')
if len(self) == 0:
print('No change in the library')
return self
def group_by(self, key):
'''Return groups of file cache diff using the key provided'''
data = list(self).sort(key=key)
return groupby(data, key=key)
def __iter__(self):
return iter(self._data)
def __getitem__(self, key):
return self._data[key]
def __len__(self):
return len(self._data)
def __repr__(self):
return f'LibraryCacheDiff(operations={[o for o in self][:2]}...)'
class LibraryCache:
def __init__(self, filepath):
self.filepath = Path(filepath)
self._data = []
@classmethod
def from_library(cls, library):
filepath = library.library_path / f"blender_assets.{library.id}.json"
return cls(filepath)
@property
def filename(self):
return self.filepath.name
@property
def library_id(self):
return self.filepath.stem.split('.')[-1]
#@property
#def filepath(self):
# """Get the filepath of the library json file relative to the library"""
# return self.directory / self.filename
def catalogs(self):
return set(a.catalog for a in self.asset_caches)
@property
def asset_caches(self):
'''Return an iterator to get all asset caches'''
return (asset_cache for file_cache in self for asset_cache in file_cache)
@property
def tmp_filepath(self):
return Path(bpy.app.tempdir) / self.filename
def read(self):
print(f'Read cache from {self.filepath}')
for file_cache_data in read_file(self.filepath):
self.add(file_cache_data)
return self
def write(self, tmp=False):
filepath = self.filepath
if tmp:
filepath = self.tmp_filepath
print(f'Write cache file to {filepath}')
write_file(filepath, self._data)
return filepath
def add(self, file_cache_data=None):
file_cache = FileCache(self, file_cache_data)
self._data.append(file_cache)
return file_cache
def add_asset_cache(self, asset_cache_data, filepath=None):
if filepath is None:
filepath = asset_cache_data['filepath']
file_cache = self.get(filepath)
if not file_cache:
file_cache = self.add()
file_cache.assets.add(asset_cache_data)
# def unflatten_cache(self, cache):
# """ Return a new unflattten list of asset data
# grouped by filepath"""
# new_cache = []
# cache = deepcopy(cache)
# cache.sort(key=lambda x : x['filepath'])
# groups = groupby(cache, key=lambda x : x['filepath'])
# keys = ['filepath', 'modified', 'library_id']
# for _, asset_datas in groups:
# asset_datas = list(asset_datas)
# #print(asset_datas[0])
# asset_info = {k:asset_datas[0][k] for k in keys}
# asset_info['assets'] = [{k:v for k, v in a.items() if k not in keys+['operation']} for a in asset_datas]
# new_cache.append(asset_info)
# return new_cache
def diff(self, new_cache=None):
"""Compare the library cache with it current state and return the cache differential"""
old_cache = self.read()
if new_cache is None:
new_cache = self
return LibraryCacheDiff(old_cache, new_cache)
def update(self, cache_diff):
#Update the cache with the operations
for asset_cache_diff in cache_diff:
file_cache = self.get(asset_cache_diff.filepath)
if not asset_cache:
print(f'Filepath {asset_cache_diff.filepath} not in {self}' )
continue
asset_cache = file_cache.get(asset_cache_diff.name)
if not asset_cache:
print(f'Asset {asset_cache_diff.name} not in file_cache {file_cache}' )
continue
if asset_cache_diff.operation == 'REMOVE':
file_cache.assets.remove(asset_cache_diff.name)
elif asset_cache_diff.operation in ('MODIFY', 'ADD'):
asset_cache.set_data(asset_cache_diff.asset_cache.to_dict())
return self
def __len__(self):
return len(self._data)
def __iter__(self):
return iter(self._data)
def __getitem__(self, key):
if isinstance(key, str):
return self.to_dict()[key]
else:
return self._data[key]
def to_dict(self):
return {a.filepath: a for a in self}
def get(self, filepath):
return next((a for a in self if a.filepath == filepath), None)
def __repr__(self):
return f'LibraryCache(library_id={self.library_id})'

View File

@ -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}
@ -14,11 +22,14 @@ ASSETLIB_FILENAME = "blender_assets.libs.json"
MODULE_DIR = Path(__file__).parent
RESOURCES_DIR = MODULE_DIR / 'resources'
LIBRARY_TYPE_DIR = MODULE_DIR / 'library_types'
LIBRARY_TYPES = []
PLUGINS_DIR = MODULE_DIR / 'plugins'
PLUGINS = {}
PLUGINS_ITEMS = [('NONE', 'None', '', 0)]
ADAPTER_DIR = MODULE_DIR / 'adapters'
ADAPTERS = []
LIB_DIR = MODULE_DIR / 'libs'
LIB_ITEMS = []
SCRIPTS_DIR = MODULE_DIR / 'scripts'
PREVIEW_ASSETS_SCRIPT = MODULE_DIR / 'common' / 'preview_assets.py'

View File

@ -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
@ -10,11 +10,13 @@ Datablock = Any
import bpy
from bpy_extras import asset_utils
from asset_library.constants import RESOURCES_DIR
#from asset_library.constants import RESOURCES_DIR
#from asset_library.common.file_utils import no
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)] ]
@ -48,6 +50,20 @@ class attr_set():
for prop, attr, old_val in self.store:
setattr(prop, attr, old_val)
def unique_name(name, names):
if name not in names:
return name
i = 1
org_name = name
while name in names:
name = f'{org_name}.{i:03d}'
i += 1
return name
def get_overriden_col(ob, scene=None):
scn = scene or bpy.context.scene
@ -56,16 +72,21 @@ def get_overriden_col(ob, scene=None):
return next((c for c in cols if ob in c.all_objects[:]
if all(not c.override_library for c in get_col_parents(c))), None)
def get_view3d_persp():
windows = bpy.context.window_manager.windows
view_3ds = [a for w in windows for a in w.screen.areas if a.type == 'VIEW_3D']
view_3d = next((a for a in view_3ds if a.spaces.active.region_3d.view_perspective == 'PERSP'), view_3ds[0])
return view_3d
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]
@ -89,6 +110,7 @@ def biggest_asset_browser_area(screen: bpy.types.Screen) -> Optional[bpy.types.A
return max(areas, key=area_sorting_key)
def suitable_areas(screen: bpy.types.Screen) -> Iterable[bpy.types.Area]:
"""Generator, yield Asset Browser areas."""
@ -98,6 +120,7 @@ def suitable_areas(screen: bpy.types.Screen) -> Iterable[bpy.types.Area]:
continue
yield area
def area_from_context(context: bpy.types.Context) -> Optional[bpy.types.Area]:
"""Return an Asset Browser suitable for the given category.
@ -122,6 +145,7 @@ def area_from_context(context: bpy.types.Context) -> Optional[bpy.types.Area]:
return None
def activate_asset(
asset: bpy.types.Action, asset_browser: bpy.types.Area, *, deferred: bool
) -> None:
@ -131,20 +155,24 @@ def activate_asset(
assert asset_utils.SpaceAssetInfo.is_asset_browser(space_data)
space_data.activate_asset_by_id(asset, deferred=deferred)
def active_catalog_id(asset_browser: bpy.types.Area) -> str:
"""Return the ID of the catalog shown in the asset browser."""
return params(asset_browser).catalog_id
def get_asset_space_params(asset_browser: bpy.types.Area) -> bpy.types.FileAssetSelectParams:
"""Return the asset browser parameters given its Area."""
space_data = asset_browser.spaces[0]
assert asset_utils.SpaceAssetInfo.is_asset_browser(space_data)
return space_data.params
def refresh_asset_browsers():
for area in suitable_areas(bpy.context.screen):
bpy.ops.asset.library_refresh({"area": area, 'region': area.regions[3]})
def tag_redraw(screen: bpy.types.Screen) -> None:
"""Tag all asset browsers for redrawing."""
@ -186,12 +214,14 @@ def norm_value(value):
value = json.dumps(value)
return value
def norm_arg(arg_name, format=str.lower, prefix='--', separator='-'):
arg_name = norm_str(arg_name, format=format, separator=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:
@ -203,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)]
@ -223,31 +256,12 @@ def get_bl_cmd(blender=None, background=False, focus=True, blendfile=None, scrip
return cmd
def get_addon_prefs():
addon_name = __package__.split('.')[0]
return bpy.context.preferences.addons[addon_name].preferences
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_col_parents(col, root=None, cols=None):
'''Return a list of parents collections of passed col
root : Pass a collection to search in (recursive)
@ -267,6 +281,7 @@ def get_col_parents(col, root=None, cols=None):
cols = get_col_parents(col, root=sub, cols=cols)
return cols
def get_overriden_col(ob, scene=None):
'''Get the collection use for making the override'''
scn = scene or bpy.context.scene
@ -276,6 +291,7 @@ def get_overriden_col(ob, scene=None):
return next((c for c in cols if ob in c.all_objects[:]
if all(not c.override_library for c in get_col_parents(c))), None)
def load_assets_from(filepath: Path) -> List[Datablock]:
if not has_assets(filepath):
# Avoid loading any datablocks when there are none marked as asset.
@ -306,6 +322,7 @@ def load_assets_from(filepath: Path) -> List[Datablock]:
loaded_assets.append(datablock)
return loaded_assets
def has_assets(filepath: Path) -> bool:
with bpy.data.libraries.load(str(filepath), assets_only=True) as (
data_from,
@ -318,17 +335,13 @@ def has_assets(filepath: Path) -> bool:
return False
def copy_frames(start, end, offset, path):
for i in range (start, end):
src = path.replace('####', f'{i:04d}')
dst = src.replace(src.split('_')[-1].split('.')[0], f'{i+offset:04d}')
shutil.copy2(src, dst)
def split_path(path) :
try :
bone_name = path.split('["')[1].split('"]')[0]
@ -342,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:
@ -477,4 +493,86 @@ def get_object_libraries(ob):
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
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:

View File

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

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)

26
data_type/__init__.py Normal file
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

@ -0,0 +1,16 @@
from asset_library.data_type.action import (
keymaps,
operators,
)
import bpy
def register():
operators.register()
keymaps.register()
def unregister():
operators.unregister()
keymaps.unregister()

View File

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

View File

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

View File

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

View File

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

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)

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.io_utils import ExportHelper, ImportHelper
from asset_library.action.functions import (
from asset_library.data_type.action.functions import (
get_marker,
get_keyframes,
)

Binary file not shown.

333
gui.py
View File

@ -1,333 +0,0 @@
# SPDX-License-Identifier: GPL-2.0-or-later
"""
Action Library - GUI definition.
"""
import bpy
from pathlib import Path
from bpy.types import (
AssetHandle,
Context,
Header,
Menu,
Panel,
UIList,
WindowManager,
WorkSpace,
)
from bpy_extras import asset_utils
from asset_library.common.bl_utils import (
get_addon_prefs,
get_object_libraries,
)
from asset_library.common.functions import (
get_active_library
)
def pose_library_panel_poll():
return bpy.context.object and bpy.context.object.mode == 'POSE'
class PoseLibraryPanel:
@classmethod
def pose_library_panel_poll(cls, context: Context) -> bool:
return bool(
context.object
and context.object.mode == 'POSE'
)
@classmethod
def poll(cls, context: Context) -> bool:
return cls.pose_library_panel_poll(context);
class AssetLibraryMenu:
@classmethod
def poll(cls, context):
from bpy_extras.asset_utils import SpaceAssetInfo
return SpaceAssetInfo.is_asset_browser_poll(context)
class ASSETLIB_PT_libraries(Panel):
bl_label = "Libraries"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = 'Item'
@classmethod
def poll(cls, context: Context) -> bool:
return context.object and get_object_libraries(context.object)
def draw(self, context: Context) -> None:
layout = self.layout
for f in get_object_libraries(context.object):
row = layout.row(align=True)
row.label(text=f)
row.operator("assetlib.open_blend", icon='FILE_BLEND', text='').filepath = f
'''
class ASSETLIB_PT_pose_library_usage(Panel):
bl_space_type = 'FILE_BROWSER'
bl_region_type = "TOOLS"
bl_label = "Action Library"
# asset_categories = {'ANIMATIONS'}
@classmethod
def poll(cls, context: Context) -> bool:
sp = context.space_data
if not context.object or not context.object.mode == 'POSE':
return False
if not (sp and sp.type == 'FILE_BROWSER' and sp.browse_mode == 'ASSETS'):
return False
return True
def draw(self, context: Context) -> None:
layout = self.layout
wm = context.window_manager
sp = context.space_data
sp.params.asset_library_ref
if sp.params.asset_library_ref == 'LOCAL':
col = layout.column(align=True)
row = col.row(align=True)
row.operator("poselib.create_pose_asset", text="Create Pose", icon='POSE_HLT').activate_new_action = False
row.operator("actionlib.replace_pose", text='Replace Pose', icon='FILE_REFRESH')
col.operator("actionlib.create_anim_asset", text="Create Anim", icon='ANIM')
col.separator()
row = col.row(align=True)
row.operator("actionlib.edit_action", text='Edit Action', icon='ACTION')
row.operator("actionlib.clear_action", text='Finish Edit', icon='CHECKBOX_HLT')
col.separator()
col.operator("actionlib.generate_preview", icon='RESTRICT_RENDER_OFF', text="Generate Thumbnail")
col.operator("actionlib.update_action_data", icon='FILE_TEXT', text="Update Action Data")
else:
col = layout.column(align=True)
row = col.row(align=True)
row.operator("actionlib.store_anim_pose", text='Store Anim/Pose', icon='ACTION')
'''
class ASSETLIB_PT_pose_library_editing(PoseLibraryPanel, asset_utils.AssetBrowserPanel, Panel):
bl_space_type = 'FILE_BROWSER'
bl_region_type = "TOOL_PROPS"
bl_label = "Metadata"
#bl_options = {'HIDE_HEADER'}
# asset_categories = {'ANIMATIONS'}
@classmethod
def poll(cls, context: Context) -> bool:
sp = context.space_data
if not (sp and sp.type == 'FILE_BROWSER' and sp.browse_mode == 'ASSETS'):
return False
if not (context.active_file and context.active_file.asset_data):
return False
return True
def draw(self, context: Context) -> None:
layout = self.layout
layout.use_property_split = True
asset_data = context.active_file.asset_data
metadata = ['camera', 'is_single_frame', 'rest_pose']
if 'camera' in asset_data.keys():
layout.prop(asset_data, f'["camera"]', text='Camera', icon='CAMERA_DATA')
if 'is_single_frame' in asset_data.keys():
layout.prop(asset_data, f'["is_single_frame"]', text='Is Single Frame')
if 'rest_pose' in asset_data.keys():
layout.prop(asset_data, f'["rest_pose"]', text='Rest Pose', icon='ACTION')
if 'filepath' in asset_data.keys():
layout.prop(asset_data, f'["filepath"]', text='Filepath')
class ASSETLIB_MT_context_menu(AssetLibraryMenu, Menu):
bl_label = "Asset Library Menu"
@classmethod
def poll(cls, context):
if not asset_utils.SpaceAssetInfo.is_asset_browser(context.space_data):
cls.poll_message_set("Current editor is not an asset browser")
return False
prefs = get_addon_prefs()
asset_lib_ref = context.space_data.params.asset_library_ref
lib = get_active_library()
if not lib:
return False
return True
def draw(self, context):
lib = get_active_library()
lib.library_type.draw_context_menu(self.layout)
def is_option_region_visible(context, space):
from bpy_extras.asset_utils import SpaceAssetInfo
if SpaceAssetInfo.is_asset_browser(space):
pass
# For the File Browser, there must be an operator for there to be options
# (irrelevant for the Asset Browser).
elif not space.active_operator:
return False
for region in context.area.regions:
if region.type == 'TOOL_PROPS' and region.width <= 1:
return False
return True
def draw_assetbrowser_header(self, context):
lib = get_active_library()
if not lib:
bpy.types.FILEBROWSER_HT_header._draw_asset_browser_buttons(self, context)
return
space_data = context.space_data
params = context.space_data.params
row = self.layout.row(align=True)
row.separator()
row.operator("assetlib.bundle", icon='UV_SYNC_SELECT', text='').name = lib.name
#op
#op.clean = False
#op.only_recent = True
lib.library_type.draw_header(row)
if context.selected_files and context.active_file:
row.separator()
row.label(text=context.active_file.name)
row.separator_spacer()
sub = row.row()
sub.ui_units_x = 10
sub.prop(params, "filter_search", text="", icon='VIEWZOOM')
row.separator_spacer()
row.prop_with_popover(
params,
"display_type",
panel="ASSETBROWSER_PT_display",
text="",
icon_only=True,
)
row.operator(
"screen.region_toggle",
text="",
icon='PREFERENCES',
depress=is_option_region_visible(context, space_data)
).region_type = 'TOOL_PROPS'
### Messagebus subscription to monitor asset library changes.
_msgbus_owner = object()
def _on_asset_library_changed() -> None:
"""Update areas when a different asset library is selected."""
refresh_area_types = {'DOPESHEET_EDITOR', 'VIEW_3D'}
for win in bpy.context.window_manager.windows:
for area in win.screen.areas:
if area.type not in refresh_area_types:
continue
area.tag_redraw()
def register_message_bus() -> None:
bpy.msgbus.subscribe_rna(
key=(bpy.types.FileAssetSelectParams, "asset_library_ref"),
owner=_msgbus_owner,
args=(),
notify=_on_asset_library_changed,
options={'PERSISTENT'},
)
def unregister_message_bus() -> None:
bpy.msgbus.clear_by_owner(_msgbus_owner)
@bpy.app.handlers.persistent
def _on_blendfile_load_pre(none, other_none) -> None:
# The parameters are required, but both are None.
unregister_message_bus()
@bpy.app.handlers.persistent
def _on_blendfile_load_post(none, other_none) -> None:
# The parameters are required, but both are None.
register_message_bus()
classes = (
ASSETLIB_PT_pose_library_editing,
#ASSETLIB_PT_pose_library_usage,
ASSETLIB_MT_context_menu,
ASSETLIB_PT_libraries
)
def register() -> None:
for cls in classes:
bpy.utils.register_class(cls)
bpy.types.FILEBROWSER_HT_header._draw_asset_browser_buttons = bpy.types.FILEBROWSER_HT_header.draw_asset_browser_buttons
bpy.types.FILEBROWSER_HT_header.draw_asset_browser_buttons = draw_assetbrowser_header
#WorkSpace.active_pose_asset_index = bpy.props.IntProperty(
# name="Active Pose Asset",
# # TODO explain which list the index belongs to, or how it can be used to get the pose.
# description="Per workspace index of the active pose asset"
#)
# Register for window-manager. This is a global property that shouldn't be
# written to files.
#WindowManager.pose_assets = bpy.props.CollectionProperty(type=AssetHandle)
# bpy.types.UI_MT_list_item_context_menu.prepend(pose_library_list_item_context_menu)
# bpy.types.ASSETLIB_MT_context_menu.prepend(pose_library_list_item_context_menu)
# bpy.types.ACTIONLIB_MT_context_menu.prepend(pose_library_list_item_context_menu)
#bpy.types.ASSETBROWSER_MT_editor_menus.append(draw_assetbrowser_header)
register_message_bus()
bpy.app.handlers.load_pre.append(_on_blendfile_load_pre)
bpy.app.handlers.load_post.append(_on_blendfile_load_post)
def unregister() -> None:
for cls in reversed(classes):
bpy.utils.unregister_class(cls)
bpy.types.FILEBROWSER_HT_header.draw_asset_browser_buttons = bpy.types.FILEBROWSER_HT_header._draw_asset_browser_buttons
del bpy.types.FILEBROWSER_HT_header._draw_asset_browser_buttons
unregister_message_bus()
#del WorkSpace.active_pose_asset_index
#del WindowManager.pose_assets
# bpy.types.UI_MT_list_item_context_menu.remove(pose_library_list_item_context_menu)
# bpy.types.ASSETLIB_MT_context_menu.remove(pose_library_list_item_context_menu)
# bpy.types.ACTIONLIB_MT_context_menu.remove(pose_library_list_item_context_menu)
#bpy.types.ASSETBROWSER_MT_editor_menus.remove(draw_assetbrowser_header)

View File

@ -1,58 +0,0 @@
# SPDX-License-Identifier: GPL-2.0-or-later
from typing import List, Tuple
import bpy
from bpy.app.handlers import persistent
addon_keymaps: List[Tuple[bpy.types.KeyMap, bpy.types.KeyMapItem]] = []
@persistent
def copy_play_anim(dummy):
wm = bpy.context.window_manager
km = wm.keyconfigs.addon.keymaps.new(name="File Browser Main", space_type="FILE_BROWSER")
km_frames = wm.keyconfigs.user.keymaps.get('Frames')
if km_frames:
play = km_frames.keymap_items.get('screen.animation_play')
if play:
kmi = km.keymap_items.new(
"assetlib.play_preview",
play.type, play.value,
any=play.any, shift=play.shift, ctrl=play.ctrl, alt=play.alt,
oskey=play.oskey, key_modifier=play.key_modifier,
)
addon_keymaps.append((km, kmi))
def register() -> None:
wm = bpy.context.window_manager
if wm.keyconfigs.addon is None:
# This happens when Blender is running in the background.
return
km = wm.keyconfigs.addon.keymaps.new(name="File Browser Main", space_type="FILE_BROWSER")
kmi = km.keymap_items.new("wm.call_menu", "RIGHTMOUSE", "PRESS")
kmi.properties.name = 'ASSETLIB_MT_context_menu'
addon_keymaps.append((km, kmi))
kmi = km.keymap_items.new("assetlib.play_preview", "SPACE", "PRESS")
addon_keymaps.append((km, kmi))
# km = addon.keymaps.new(name = "Grease Pencil Stroke Paint Mode", space_type = "EMPTY")
# kmi = km.keymap_items.new('wm.call_panel', type='F2', value='PRESS')
if 'copy_play_anim' not in [hand.__name__ for hand in bpy.app.handlers.load_post]:
bpy.app.handlers.load_post.append(copy_play_anim)
def unregister() -> None:
# Clear shortcuts from the keymap.
if 'copy_play_anim' in [hand.__name__ for hand in bpy.app.handlers.load_post]:
bpy.app.handlers.load_post.remove(copy_play_anim)
for km, kmi in addon_keymaps:
km.keymap_items.remove(kmi)
addon_keymaps.clear()

View File

@ -1,17 +0,0 @@
from asset_library.library_types import library_type
from asset_library.library_types import copy_folder
from asset_library.library_types import scan_folder
if 'bpy' in locals():
import importlib
importlib.reload(library_type)
importlib.reload(copy_folder)
importlib.reload(scan_folder)
import bpy
LibraryType = library_type.LibraryType
CopyFolder = copy_folder.CopyFolder
ScanFolder = scan_folder.ScanFolder

File diff suppressed because it is too large Load Diff

0
plugins/__init__.py Normal file
View File

View File

@ -4,9 +4,9 @@ Plugin for making an asset library of all blender file found in a folder
"""
from asset_library.library_types.scan_folder import ScanFolder
from asset_library.common.bl_utils import load_datablocks
from asset_library.common.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)

View File

@ -3,15 +3,18 @@
Adapter for making an asset library of all blender file found in a folder
"""
from asset_library.library_types.library_type import LibraryType
from asset_library.common.file_utils import copy_dir
from bpy.props import StringProperty
from os.path import expandvars
import bpy
from bpy.props import StringProperty
from asset_library.plugins.library_plugin import LibraryPlugin
from asset_library.core.file_utils import copy_dir
class CopyFolder(LibraryType):
class CopyFolder(LibraryPlugin):
"""Copy library folder from a server to a local disk for better performance"""
name = "Copy Folder"

0
plugins/data_file.py Normal file
View File

View File

@ -3,13 +3,6 @@
Plugin for making an asset library of all blender file found in a folder
"""
from asset_library.library_types.library_type import LibraryType
from asset_library.common.template import Template
from asset_library.common.file_utils import install_module
import bpy
from bpy.props import (StringProperty, IntProperty, BoolProperty)
import re
from pathlib import Path
from itertools import groupby
@ -21,8 +14,15 @@ import urllib3
import traceback
import time
import bpy
from bpy.props import (StringProperty, IntProperty, BoolProperty)
class Kitsu(LibraryType):
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):
name = "Kitsu"
template_name : StringProperty()

View File

@ -1,42 +1,43 @@
#from asset_library.common.functions import (norm_asset_datas,)
from asset_library.common.bl_utils import get_addon_prefs, load_datablocks
from asset_library.common.file_utils import read_file, write_file
from asset_library.common.template import Template
from asset_library.constants import (MODULE_DIR, RESOURCES_DIR)
from asset_library import (action, collection, file)
from asset_library.common.library_cache import LibraryCacheDiff
from bpy.types import PropertyGroup
from bpy.props import StringProperty
import bpy
from bpy_extras import asset_utils
from itertools import groupby
from pathlib import Path
import shutil
import os
import shutil
import json
import uuid
import time
from functools import partial
import subprocess
from pathlib import Path
from itertools import groupby
from functools import partial
from glob import glob
from copy import deepcopy
import bpy
from bpy_extras import asset_utils
from bpy.types import PropertyGroup
from bpy.props import StringProperty
class LibraryType(PropertyGroup):
#from asset_library.common.functions import (norm_asset_datas,)
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 asset_library.data_type import (action, collection, file)
#from asset_library.common.library_cache import LibraryCacheDiff
class LibraryPlugin(PropertyGroup):
#def __init__(self):
name = "Base Adapter"
#name = "Base Adapter"
#library = None
@property
def library(self):
prefs = self.addon_prefs
for lib in prefs.libraries:
if lib.library_type == self:
if lib.plugin == self:
return lib
@property
@ -104,7 +105,7 @@ class LibraryType(PropertyGroup):
return self.library.read_cache(filepath=filepath)
def fetch(self):
raise Exception('This method need to be define in the library_type')
raise Exception('This method need to be define in the plugin')
def norm_file_name(self, name):
return name.replace(' ', '_')
@ -186,7 +187,7 @@ class LibraryType(PropertyGroup):
if 'filepath' in asset_handle.asset_data:
asset_path = asset_handle.asset_data['filepath']
asset_path = lib.library_type.format_path(asset_path)
asset_path = lib.plugin.format_path(asset_path)
else:
asset_path = bpy.types.AssetHandle.get_full_library_path(
asset_handle, bpy.context.asset_library_ref
@ -195,22 +196,22 @@ class LibraryType(PropertyGroup):
return asset_path
def generate_previews(self):
raise Exception('Need to be defined in the library_type')
raise Exception('Need to be defined in the plugin')
def get_image_path(self, name, catalog, filepath):
raise Exception('Need to be defined in the library_type')
raise Exception('Need to be defined in the plugin')
def get_video_path(self, name, catalog, filepath):
raise Exception('Need to be defined in the library_type')
raise Exception('Need to be defined in the plugin')
def new_asset(self, asset, asset_cache):
raise Exception('Need to be defined in the library_type')
raise Exception('Need to be defined in the plugin')
def remove_asset(self, asset, asset_cache):
raise Exception('Need to be defined in the library_type')
raise Exception('Need to be defined in the plugin')
def set_asset_preview(self, asset, asset_cache):
raise Exception('Need to be defined in the library_type')
raise Exception('Need to be defined in the plugin')
def format_asset_data(self, data):
"""Get a dict for use in template fields"""
@ -321,7 +322,7 @@ class LibraryType(PropertyGroup):
# return write_file(cache_path, list(asset_infos))
def prop_rel_path(self, path, prop):
'''Get a filepath relative to a property of the library_type'''
'''Get a filepath relative to a property of the plugin'''
field_prop = '{%s}/'%prop
prop_value = getattr(self, prop)
@ -595,7 +596,7 @@ class LibraryType(PropertyGroup):
# Write the cache in a temporary file for the generate preview script
tmp_cache_file = cache.write(tmp=True)
bpy.ops.assetlib.generate_previews(name=self.library.name, cache=str(tmp_cache_file))
bpy.ops.assetlibrary.generate_previews(name=self.library.name, cache=str(tmp_cache_file))
elif isinstance(cache_diff, (Path, str)):
cache_diff = LibraryCacheDiff(cache_diff).read()#json.loads(Path(cache_diff).read_text(encoding='utf-8'))
@ -764,7 +765,7 @@ class LibraryType(PropertyGroup):
# return list(new_cache.values()), cache_diff
def draw_prefs(self, layout):
"""Draw the options in the addon preference for this library_type"""
"""Draw the options in the addon preference for this plugin"""
annotations = self.__class__.__annotations__
for k, v in annotations.items():

View File

@ -3,31 +3,31 @@
Plugin for making an asset library of all blender file found in a folder
"""
from asset_library.library_types.library_type import LibraryType
from asset_library.common.template import Template
from asset_library.common.file_utils import install_module
import bpy
from bpy.props import (StringProperty, IntProperty, BoolProperty, EnumProperty)
import re
from pathlib import Path
from itertools import groupby
import uuid
import os
import re
import uuid
import shutil
import json
import requests
import urllib3
import traceback
import time
from itertools import groupby
from pathlib import Path
from pprint import pprint as pp
import bpy
from bpy.props import (StringProperty, IntProperty, BoolProperty, EnumProperty)
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()
REQ_HEADERS.update({"User-Agent": "Blender: PH Assets"})
class PolyHaven(LibraryType):
class PolyHaven(LibraryPlugin):
name = "Poly Haven"
# template_name : StringProperty()

View File

@ -3,24 +3,25 @@
Plugin for making an asset library of all blender file found in a folder
"""
from asset_library.library_types.library_type import LibraryType
from asset_library.common.bl_utils import load_datablocks
from asset_library.common.template import Template
import bpy
from bpy.props import (StringProperty, IntProperty, BoolProperty)
import re
from pathlib import Path
from itertools import groupby
import uuid
import os
import shutil
import json
import time
from pathlib import Path
from itertools import groupby
import bpy
from bpy.props import (StringProperty, IntProperty, BoolProperty)
from asset_library.plugins.library_plugin import LibraryPlugin
from asset_library.core.bl_utils import load_datablocks
from asset_library.core.template import Template
class ScanFolder(LibraryType):
class ScanFolder(LibraryPlugin):
name = "Scan Folder"
source_directory : StringProperty(subtype='DIR_PATH')
@ -57,10 +58,10 @@ class ScanFolder(LibraryType):
return self.format_path(self.source_template_video, dict(name=name, catalog=catalog, filepath=filepath))
def new_asset(self, asset, asset_data):
raise Exception('Need to be defined in the library_type')
raise Exception('Need to be defined in the plugin')
def remove_asset(self, asset, asset_data):
raise Exception('Need to be defined in the library_type')
raise Exception('Need to be defined in the plugin')
'''
def format_asset_info(self, asset_datas, asset_path, modified=None):
@ -140,7 +141,7 @@ class ScanFolder(LibraryType):
# Write the cache in a temporary file for the generate preview script
tmp_cache_file = cache.write(tmp=True)
bpy.ops.assetlib.generate_previews(name=self.library.name, cache=str(tmp_cache_file))
bpy.ops.assetlibrary.generate_previews(name=self.library.name, cache=str(tmp_cache_file))
elif isinstance(cache_diff, (Path, str)):
cache_diff = json.loads(Path(cache_diff).read_text(encoding='utf-8'))

View File

@ -1,774 +1,59 @@
import bpy
import os
from os.path import abspath, join
from bpy.types import (AddonPreferences, PointerProperty, PropertyGroup)
from bpy.props import (BoolProperty, StringProperty, CollectionProperty,
EnumProperty, IntProperty)
from asset_library.constants import (DATA_TYPES, DATA_TYPE_ITEMS,
ICONS, RESOURCES_DIR, LIBRARY_TYPE_DIR, LIBRARY_TYPES, ADAPTERS)
from asset_library.common.file_utils import import_module_from_path, norm_str
from asset_library.common.bl_utils import get_addon_prefs
from asset_library.common.library_cache import LibraryCache
from asset_library.common.catalog import Catalog
#from asset_library.common.functions import get_catalog_path
from pathlib import Path
import importlib
import inspect
def update_library_config(self, context):
print('update_library_config not yet implemented')
def update_library_path(self, context):
prefs = get_addon_prefs()
self['bundle_directory'] = str(self.library_path)
if not self.custom_bundle_name:
self['custom_bundle_name'] = self.name
if not self.custom_bundle_directory:
custom_bundle_dir = Path(prefs.bundle_directory, self.library_name).resolve()
self['custom_bundle_directory'] = str(custom_bundle_dir)
#if self.custom_bundle_directory:
# self['custom_bundle_directory'] = abspath(bpy.path.abspath(self.custom_bundle_directory))
#else:
# bundle_directory = join(prefs.bundle_directory, norm_str(self.name))
# self['custom_bundle_directory'] = abspath(bundle_directory)
self.set_library_path()
def update_all_library_path(self, context):
#print('update_all_assetlib_paths')
prefs = get_addon_prefs()
#if self.custom_bundle_directory:
# self['custom_bundle_directory'] = abspath(bpy.path.abspath(self.custom_bundle_directory))
for lib in prefs.libraries:
update_library_path(lib, context)
#lib.set_library_path()
def get_library_type_items(self, context):
#prefs = get_addon_prefs()
items = [('NONE', 'None', '', 0)]
items += [(norm_str(a.name, format=str.upper), a.name, "", i+1) for i, a in enumerate(LIBRARY_TYPES)]
return items
def get_adapters_items(self, context):
#prefs = get_addon_prefs()
items = [('NONE', 'None', '', 0)]
items += [(norm_str(a.name, format=str.upper), a.name, "", i+1) for i, a in enumerate(ADAPTERS)]
return items
def get_library_items(self, context):
prefs = get_addon_prefs()
items = [('NONE', 'None', '', 0)]
items += [(l.name, l.name, "", i+1) for i, l in enumerate(prefs.libraries) if l != self]
return items
def get_store_library_items(self, context):
#prefs = get_addon_prefs()
#libraries = [l for l in prefs.libraries if l.merge_library == self.name]
return [(l.name, l.name, "", i) for i, l in enumerate([self] + self.merge_libraries)]
class LibraryTypes(PropertyGroup):
def __iter__(self):
return (getattr(self, p) for p in self.bl_rna.properties.keys() if p not in ('rna_type', 'name'))
class Adapters(PropertyGroup):
def __iter__(self):
return (getattr(self, p) for p in self.bl_rna.properties.keys() if p not in ('rna_type', 'name'))
class AssetLibrary(PropertyGroup):
name : StringProperty(name='Name', default='Action Library', update=update_library_path)
id : StringProperty()
auto_bundle : BoolProperty(name='Auto Bundle', default=False)
expand : BoolProperty(name='Expand', default=False)
use : BoolProperty(name='Use', default=True, update=update_library_path)
data_type : EnumProperty(name='Type', items=DATA_TYPE_ITEMS, default='COLLECTION')
#template_image : StringProperty(default='', description='../{name}_image.png')
#template_video : StringProperty(default='', description='../{name}_video.mov')
#template_info : StringProperty(default='', description='../{name}_asset_info.json')
bundle_directory : StringProperty(
name="Bundle Directory",
subtype='DIR_PATH',
default=''
)
use_custom_bundle_directory : BoolProperty(default=False, update=update_library_path)
custom_bundle_directory : StringProperty(
name="Bundle Directory",
subtype='DIR_PATH',
default='',
update=update_library_path
)
#use_merge : BoolProperty(default=False, update=update_library_path)
use_custom_bundle_name : BoolProperty(default=False, update=update_library_path)
custom_bundle_name : StringProperty(name='Merge Name', update=update_library_path)
#merge_library : EnumProperty(name='Merge Library', items=get_library_items, update=update_library_path)
#merge_name : StringProperty(name='Merge Name', update=update_library_path)
#Library when adding an asset to the library if merge with another
store_library: EnumProperty(items=get_store_library_items, name="Library")
template: StringProperty()
expand_extra : BoolProperty(name='Expand', default=False)
blend_depth : IntProperty(name='Blend Depth', default=1)
# source_directory : StringProperty(
# name="Path",
# subtype='DIR_PATH',
# default='',
# update=update_library_path
# )
#library_type : EnumProperty(items=library_type_ITEMS)
library_types : bpy.props.PointerProperty(type=LibraryTypes)
library_type_name : EnumProperty(items=get_library_type_items)
adapters : bpy.props.PointerProperty(type=Adapters)
adapter_name : EnumProperty(items=get_adapters_items)
parent_name : StringProperty()
# data_file_path : StringProperty(
# name="Path",
# subtype='FILE_PATH',
# default='',
# )
#def __init__(self):
# self.library_types.parent = self
@property
def parent(self):
prefs = get_addon_prefs()
if self.parent_name:
return prefs.libraries[self.parent_name]
@property
def merge_libraries(self):
prefs = get_addon_prefs()
return [l for l in prefs.libraries if l != self and (l.library_path == self.library_path)]
@property
def child_libraries(self):
prefs = get_addon_prefs()
return [l for l in prefs.libraries if l != self and (l.parent == self)]
@property
def data_types(self):
data_type = self.data_type
if data_type == 'FILE':
data_type = 'COLLECTION'
return f'{data_type.lower()}s'
@property
def library_type(self):
name = norm_str(self.library_type_name)
if not hasattr(self.library_types, name):
return
return getattr(self.library_types, name)
@property
def adapter(self):
name = norm_str(self.adapter_name)
if not hasattr(self.adapters, name):
return
return getattr(self.adapters, name)
@property
def library(self):
prefs = get_addon_prefs()
asset_lib_ref = bpy.context.space_data.params.asset_library_ref
#TODO work also outside asset_library_area
if asset_lib_ref not in prefs.libraries:
return None
return prefs.libraries[asset_lib_ref]
@property
def library_path(self):
prefs = get_addon_prefs()
library_name = self.library_name
#if not self.use_custom_bundle_name:
# library_name = norm_str(library_name)
if self.use_custom_bundle_directory:
return Path(self.custom_bundle_directory).resolve()
else:
library_name = norm_str(library_name)
return Path(prefs.bundle_directory, library_name).resolve()
@property
def bundle_dir(self):
return self.library_path.as_posix()
@property
def library_name(self):
if self.use_custom_bundle_name:
return self.custom_bundle_name
return self.name
def read_catalog(self):
return Catalog(self.library_path).read()
def read_cache(self, filepath=None):
if filepath:
return LibraryCache(filepath).read()
return LibraryCache.from_library(self).read()
def clear_library_path(self):
#print('Clear Library Path', self.name)
prefs = bpy.context.preferences
libs = prefs.filepaths.asset_libraries
#path = self.library_path.as_posix()
for l in reversed(libs):
#lib_path = Path(l.path).resolve().as_posix()
prev_name = self.get('asset_library') or self.library_name
#print(l.name, prev_name)
if (l.name == prev_name):
index = list(libs).index(l)
try:
bpy.ops.preferences.asset_library_remove(index=index)
return
except AttributeError:
pass
#print('No library removed')
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()
#print(obj)
for key, value in data.items():
if isinstance(value, dict):
if 'name' in value:
setattr(obj, f'{key}_name', value.pop('name'))
#print('Nested value', getattr(obj, key))
self.set_dict(value, obj=getattr(obj, key))
elif key in obj.bl_rna.properties.keys():
if key == 'id':
value = str(value)
elif key == 'custom_bundle_name':
if not 'use_custom_bundle_name' in data.values():
obj["use_custom_bundle_name"] = True
elif isinstance(value, str):
value = os.path.expandvars(value)
value = os.path.expanduser(value)
#print('set attr', key, value)
setattr(obj, key, value)
#obj[key] = value
else:
print(f'Prop {key} of {obj} not exist')
self['bundle_directory'] = str(self.library_path)
if not self.custom_bundle_name:
self['custom_bundle_name'] = self.name
# self.library_type_name = data['library_type']
# if not self.library_type:
# print(f"No library_type named {data['library_type']}")
# return
# for key, value in data.items():
# if key == 'options':
# for k, v in data['options'].items():
# setattr(self.library_type, k, v)
# elif key in self.bl_rna.properties.keys():
# if key == 'id':
# value = str(value)
# if key == 'custom_bundle_name':
# if not 'use_custom_bundle_name' in data.values():
# self["use_custom_bundle_name"] = True
# self[key] = value
def to_dict(self):
data = {p: getattr(self, p) for p in self.bl_rna.properties.keys() if p !='rna_type'}
if self.library_type:
data['library_type'] = self.library_type.to_dict()
data['library_type']['name'] = data.pop('library_type_name')
del data['library_types']
if self.adapter:
data['adapter'] = self.adapter.to_dict()
data['adapter']['name'] = data.pop('adapter_name')
del data['adapters']
return data
def set_library_path(self):
'''Update the Blender Preference Filepaths tab with the addon libraries'''
prefs = bpy.context.preferences
name = self.library_name
lib_path = self.library_path
self.clear_library_path()
if not self.use or not lib_path:
# if all(not l.use for l in self.merge_libraries):
# self.clear_library_path()
return
# lib = None
# if self.get('asset_library'):
# #print('old_name', self['asset_library'])
# lib = prefs.filepaths.asset_libraries.get(self['asset_library'])
# if not lib:
# #print('keys', prefs.filepaths.asset_libraries.keys())
# #print('name', name)
# #print(prefs.filepaths.asset_libraries.get(name))
# lib = prefs.filepaths.asset_libraries.get(name)
# Create the Asset Library Path
lib = prefs.filepaths.asset_libraries.get(name)
if not lib:
#print(f'Creating the lib {name}')
try:
bpy.ops.preferences.asset_library_add(directory=str(lib_path))
except AttributeError:
return
lib = prefs.filepaths.asset_libraries[-1]
lib.name = name
self['asset_library'] = name
lib.path = str(lib_path)
@property
def is_user(self):
prefs = get_addon_prefs()
return self in prefs.user_libraries.values()
@property
def is_env(self):
prefs = get_addon_prefs()
return self in prefs.env_libraries.values()
def add_row(self, layout, data=None, prop=None, label='',
boolean=None, factor=0.39):
'''Act like the use_property_split but with more control'''
enabled = True
split = layout.split(factor=factor, align=True)
row = split.row(align=False)
row.use_property_split = False
row.alignment= 'RIGHT'
row.label(text=str(label))
if boolean:
boolean_data = self
if isinstance(boolean, (list, tuple)):
boolean_data, boolean = boolean
row.prop(boolean_data, boolean, text='')
enabled = getattr(boolean_data, boolean)
row = split.row(align=True)
row.enabled = enabled
if isinstance(data, str):
row.label(text=data)
else:
row.prop(data or self, prop, text='')
return split
def draw_operators(self, layout):
row = layout.row(align=True)
row.alignment = 'RIGHT'
row.prop(self, 'library_type_name', text='')
row.prop(self, 'auto_bundle', text='', icon='UV_SYNC_SELECT')
row.operator("assetlib.diff", text='', icon='FILE_REFRESH').name = self.name
op = row.operator("assetlib.bundle", icon='MOD_BUILD', text='')
op.name = self.name
layout.separator(factor=3)
def draw(self, layout):
prefs = get_addon_prefs()
#box = layout.box()
row = layout.row(align=True)
#row.use_property_split = False
#row.alignment = 'LEFT'
icon = "DISCLOSURE_TRI_DOWN" if self.expand else "DISCLOSURE_TRI_RIGHT"
row.prop(self, 'expand', icon=icon, emboss=False, text='')
if self.is_user:
row.prop(self, 'use', text='')
row.prop(self, 'data_type', icon_only=True, emboss=False)
row.prop(self, 'name', text='')
self.draw_operators(row)
index = list(prefs.user_libraries).index(self)
row.operator("assetlib.remove_user_library", icon="X", text='', emboss=False).index = index
else:
row.prop(self, 'use', text='')
row.label(icon=ICONS[self.data_type])
#row.label(text=self.name)
subrow = row.row(align=True)
subrow.alignment = 'LEFT'
subrow.prop(self, 'expand', emboss=False, text=self.name)
#row.separator_spacer()
self.draw_operators(row)
sub_row = row.row()
sub_row.enabled = False
sub_row.label(icon='FAKE_USER_ON')
if self.expand:
col = layout.column(align=False)
col.use_property_split = True
#row = col.row(align=True)
row = self.add_row(col,
prop="custom_bundle_name",
boolean="use_custom_bundle_name",
label='Custom Bundle Name')
row.enabled = not self.use_custom_bundle_directory
prop = "bundle_directory"
if self.use_custom_bundle_directory:
prop = "custom_bundle_directory"
self.add_row(col, prop=prop,
boolean="use_custom_bundle_directory",
label='Custom Bundle Directory',
)
col.prop(self, "blend_depth")
#subcol = col.column(align=True)
#subcol.prop(self, "template_info", text='Template Info', icon='COPY_ID')
#subcol.prop(self, "template_image", text='Template Image', icon='COPY_ID')
#subcol.prop(self, "template_video", text='Template Video', icon='COPY_ID')
if self.library_type:
col.separator()
self.library_type.draw_prefs(col)
for lib in self.child_libraries:
lib.draw(layout)
col.separator()
class Collections:
'''Util Class to merge multiple collections'''
collections = []
def __init__(self, *collection):
self.collections = collection
for col in collection:
#print('Merge methods')
for attr in dir(col):
if attr.startswith('_'):
continue
value = getattr(col, attr)
#if not callable(value):
# continue
setattr(self, attr, value)
def __contains__(self, item):
if isinstance(item, str):
return item in self.to_dict()
else:
return item in self
def __iter__(self):
return self.to_list().__iter__()
def __getitem__(self, item):
if isinstance(item, int):
return self.to_list()[item]
else:
return self.to_dict()[item]
def get(self, item, fallback=None):
return self.to_dict().get(item) or fallback
def to_dict(self):
return {k:v for c in self.collections for k, v in c.items()}
def to_list(self):
return [v for c in self.collections for v in c.values()]
def get_parent(self, item):
for c in self.collections:
if item in c.values():
return c
def index(self, item):
c = self.get_parent(item)
if not c:
return item in self
return list(c.values()).index(item)
#class AssetLibraryOptions(PropertyGroup):
# pass
class AssetLibraryPrefs(AddonPreferences):
bl_idname = __package__
adapters = []
library_types = []
previews = bpy.utils.previews.new()
preview_modal = False
add_asset_dict = {}
#action : bpy.props.PointerProperty(type=AssetLibraryPath)
#asset : bpy.props.PointerProperty(type=AssetLibraryPath)
#library_types = {}
author: StringProperty(default=os.getlogin())
image_player: StringProperty(default='')
video_player: StringProperty(default='')
library_type_directory : StringProperty(name="Library Type Directory", subtype='DIR_PATH')
adapter_directory : StringProperty(name="Adapter Directory", subtype='DIR_PATH')
env_libraries : CollectionProperty(type=AssetLibrary)
user_libraries : CollectionProperty(type=AssetLibrary)
expand_settings: BoolProperty(default=False)
bundle_directory : StringProperty(
name="Path",
subtype='DIR_PATH',
default='',
update=update_all_library_path
)
config_directory : StringProperty(
name="Config Path",
subtype='FILE_PATH',
default=str(RESOURCES_DIR/"asset_library_config.json"),
update=update_library_config
)
def load_library_types(self):
from asset_library.library_types.library_type import LibraryType
print('Asset Library: Load Library Types')
LIBRARY_TYPES.clear()
library_type_files = list(LIBRARY_TYPE_DIR.glob('*.py'))
if self.library_type_directory:
user_LIBRARY_TYPE_DIR = Path(self.library_type_directory)
if user_LIBRARY_TYPE_DIR.exists():
library_type_files += list(user_LIBRARY_TYPE_DIR.glob('*.py'))
for library_type_file in library_type_files:
if library_type_file.stem.startswith('_'):
continue
mod = import_module_from_path(library_type_file)
#print(library_type_file)
for name, obj in inspect.getmembers(mod):
if not inspect.isclass(obj):
continue
#print(obj.__bases__)
if not LibraryType in obj.__mro__:
continue
# Non registering base library_type
if obj is LibraryType or obj.name in (a.name for a in LIBRARY_TYPES):
continue
try:
print(f'Register Plugin {name}')
bpy.utils.register_class(obj)
setattr(LibraryTypes, norm_str(obj.name), bpy.props.PointerProperty(type=obj))
LIBRARY_TYPES.append(obj)
except Exception as e:
print(f'Could not register library_type {name}')
print(e)
def load_adapters(self):
return
@property
def libraries(self):
return Collections(self.env_libraries, self.user_libraries)
def draw(self, context):
prefs = get_addon_prefs()
layout = self.layout
#layout.use_property_split = True
main_col = layout.column(align=False)
box = main_col.box()
row = box.row(align=True)
icon = "DISCLOSURE_TRI_DOWN" if self.expand_settings else "DISCLOSURE_TRI_RIGHT"
row.prop(self, 'expand_settings', icon=icon, emboss=False, text='')
row.label(icon='PREFERENCES')
row.label(text='Settings')
#row.separator_spacer()
subrow = row.row()
subrow.alignment = 'RIGHT'
subrow.operator("assetlib.reload_addon", text='Reload Addon')
if prefs.expand_settings:
col = box.column(align=True)
col.use_property_split = True
#col.prop(self, 'use_single_path', text='Single Path')
col.prop(self, 'bundle_directory', text='Bundle Directory')
col.separator()
col.prop(self, 'library_type_directory')
col.prop(self, 'config_directory')
col.separator()
#col.prop(self, 'template_info', text='Asset Description Template', icon='COPY_ID')
#col.separator()
#col.prop(self, 'template_image', text='Template Image', icon='COPY_ID')
col.prop(self, 'image_player', text='Image Player') #icon='OUTLINER_OB_IMAGE'
#col.separator()
#col.prop(self, 'template_video', text='Template Video', icon='COPY_ID')
col.prop(self, 'video_player', text='Video Player') #icon='FILE_MOVIE'
col.separator()
col.operator("assetlib.add_user_library", text='Bundle All Libraries', icon='MOD_BUILD')
for lib in self.libraries:# list(self.env_libraries) + list(self.user_libraries):
if lib.parent:
continue
box = main_col.box()
lib.draw(box)
row = main_col.row()
row.alignment = 'RIGHT'
row.operator("assetlib.add_user_library", icon="ADD", text='', emboss=False)
classes = [
LibraryTypes,
Adapters,
#ConformAssetLibrary,
AssetLibrary,
AssetLibraryPrefs,
]
def register():
for cls in classes:
bpy.utils.register_class(cls)
prefs = get_addon_prefs()
# Read Env and override preferences
bundle_dir = os.getenv('ASSETLIB_BUNDLE_DIR')
if bundle_dir:
prefs['bundle_directory'] = os.path.expandvars(bundle_dir)
config_dir = os.getenv('ASSETLIB_CONFIG_DIR')
if config_dir:
prefs['config_directory'] = os.path.expandvars(config_dir)
LIBRARY_TYPE_DIR = os.getenv('ASSETLIB_LIBRARY_TYPE_DIR')
if LIBRARY_TYPE_DIR:
prefs['library_type_directory'] = os.path.expandvars(LIBRARY_TYPE_DIR)
ADAPTER_DIR = os.getenv('ASSETLIB_ADAPTER_DIR')
if ADAPTER_DIR:
prefs['adapter_directory'] = os.path.expandvars(ADAPTER_DIR)
prefs.load_library_types()
prefs.load_adapters()
def unregister():
for cls in reversed(classes + LIBRARY_TYPES):
bpy.utils.unregister_class(cls)
LIBRARY_TYPES.clear()
import bpy
from bpy.types import AddonPreferences
from bpy.props import (CollectionProperty, StringProperty)
from . properties import AssetLibrary
from . core.bl_utils import get_addon_prefs
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=lambda s, c: update_library_path()
)
def draw(self, context):
prefs = get_addon_prefs()
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()
row = col.row()
row.label(text='Libraries:')
#row.alignment = 'RIGHT'
row.separator_spacer()
row.operator("assetlibrary.reload_addon", icon='FILE_REFRESH', text='')
row.operator("assetlibrary.add_library", icon="ADD", text='', emboss=False)
for lib in self.libraries:# list(self.env_libraries) + list(self.user_libraries):
lib.draw(col)
classes = (
AssetLibraryPrefs,
)
def register():
for cls in classes:
bpy.utils.register_class(cls)
def unregister():
for cls in reversed(classes):
bpy.utils.unregister_class(cls)

186
properties.py Normal file
View File

@ -0,0 +1,186 @@
import inspect
import os
import bpy
from bpy.types import (AddonPreferences, PropertyGroup)
from bpy.props import (BoolProperty, StringProperty, CollectionProperty,
EnumProperty, IntProperty, PointerProperty)
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'))
# if self.plugin_directory:
# user_plugin_DIR = Path(self.plugin_directory)
# if user_plugin_DIR.exists():
# plugin_files += list(user_plugin_DIR.glob('*.py'))
for plugin_file in plugin_files:
if plugin_file.stem.startswith('_'):
continue
mod = import_module_from_path(plugin_file)
for name, obj in inspect.getmembers(mod):
if not inspect.isclass(obj) or (obj is LibraryPlugin):
continue
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[obj.name] = obj
except Exception as e:
print(f'Could not register plugin {name}')
print(e)
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):
return (getattr(self, p) for p in self.bl_rna.properties.keys() if p not in ('rna_type', 'name'))
class AssetLibrary(PropertyGroup):
"""Library item defining one library with his plugin and settings"""
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=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()
return list(prefs.libraries).index(self)
"""
def draw_operators(self, layout):
row = layout.row(align=True)
row.alignment = 'RIGHT'
row.prop(self, 'plugin_name', text='')
row.prop(self, 'auto_bundle', text='', icon='UV_SYNC_SELECT')
row.operator("assetlibrary.diff", text='', icon='FILE_REFRESH').name = self.name
op = row.operator("assetlibrary.sync", icon='MOD_BUILD', text='')
op.name = self.name
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)
box = layout.box()
row = box.row(align=True)
icon = "DISCLOSURE_TRI_DOWN" if self.expand else "DISCLOSURE_TRI_RIGHT"
row.prop(self, 'expand', icon=icon, emboss=False, text='')
row.prop(self, 'use', text='')
#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
row.separator(factor=0.5)
row.operator("assetlibrary.remove_library", icon="REMOVE", text='', emboss=False).index = self.index
#self.draw_operators(row)
if self.expand:
col = box.column(align=False)
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 = (
Plugins,
AssetLibrary,
WindowManagerProperties
)
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)
del bpy.types.WindowManager.asset_library

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 731 B

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

View File

@ -1,270 +0,0 @@
import bpy
from pathlib import Path
from asset_library.common.file_utils import read_file, write_file
from copy import deepcopy
import time
from itertools import groupby
class AssetCache:
def __init__(self, file_cache, data):
self.file_cache = file_cache
self._data = data
self.catalog = data['catalog']
self.author = data.get('author', '')
self.description = data.get('description', '')
self.tags = data.get('tags', [])
self.type = data.get('type')
self.name = data['name']
self._metadata = data.get('metadata', {})
@property
def filepath(self):
return self.file_cache.filepath
@property
def metadata(self):
metadata = {
'.library_id': self.library.id,
'.filepath': self.filepath
}
metadata.update(self.metadata)
return metadata
@property
def norm_name(self):
return self.name.replace(' ', '_').lower()
def to_dict(self):
return dict(
catalog=self.catalog,
author=self.author,
metadata=self.metadata,
description=self.description,
tags=self.tags,
type=self.type,
name=self.name
)
def __str__(self):
return f'AssetCache(name={self.name}, type={self.type}, catalog={self.catalog})'
class FileCache:
def __init__(self, library_cache, data):
self.library_cache = library_cache
self.filepath = data['filepath']
self.modified = data.get('modified', time.time_ns())
self._data = []
for asset_cache_data in data.get('assets', []):
self.add(asset_cache_data)
def add(self, asset_cache_data):
asset_cache = AssetCache(self, asset_cache_data)
self._data.append(asset_cache)
def to_dict(self):
return dict(
filepath=self.filepath.as_posix(),
modified=self.modified,
library_id=self.library_cache.library.id,
assets=[asset_cache.to_dict() for asset_cache in self]
)
def __iter__(self):
return self._data.__iter__()
def __getitem__(self, key):
return self._data[key]
def __str__(self):
return f'FileCache(filepath={self.filepath})'
class AssetCacheDiff:
def __init__(self, library_diff, asset_cache, operation):
self.library_cache = library_cache
self.filepath = data['filepath']
self.operation = operation
class LibraryCacheDiff:
def __init__(self, filepath=None):
self.filepath = filepath
self._data = []
def add(self, asset_diff):
asset_diff = AssetCacheDiff(self, asset_diff)
self._data.append(asset_cache_diff)
def set(self, asset_diffs):
for asset_diff in asset_diffs:
self.add(asset_diff)
def read(self):
print(f'Read cache from {self.filepath}')
for asset_diff_data in read_file(self.filepath):
self.add(asset_diff_data)
return self
def group_by(self, key):
'''Return groups of file cache diff using the key provided'''
data = list(self).sort(key=key)
return groupby(data, key=key)
def __iter__(self):
return iter(self._data)
def __len__(self):
return len(self._data)
def __getitem__(self, key):
return self._data[key]
class LibraryCache:
def __init__(self, directory, id):
self.directory = directory
self.id = id
self._data = []
@classmethod
def from_library(cls, library):
return cls(library.library_path, library.id)
@property
def filename(self):
return f"blender_assets.{self.id}.json"
@property
def filepath(self):
"""Get the filepath of the library json file relative to the library"""
return self.directory / self.filename
@property
def asset_caches(self):
'''Return an iterator to get all asset caches'''
return (asset_cache for file_cache in self for asset_cache in file_cache)
@property
def tmp_filepath(self):
return Path(bpy.app.tempdir) / self.filename
def read(self):
print(f'Read cache from {self.filepath}')
for file_cache_data in read_file(self.filepath):
self.add(file_cache_data)
return self
def write(self, temp=False):
filepath = self.filepath
if temp:
filepath = self.tmp_filepath
print(f'Write cache file to {filepath}')
write_file(filepath, self._data)
return filepath
def add(self, file_cache_data):
file_cache = FileCache(self, file_cache_data)
self._data.append(file_cache)
def unflatten_cache(self, cache):
""" Return a new unflattten list of asset data
grouped by filepath"""
new_cache = []
cache = deepcopy(cache)
cache.sort(key=lambda x : x['filepath'])
groups = groupby(cache, key=lambda x : x['filepath'])
keys = ['filepath', 'modified', 'library_id']
for _, asset_datas in groups:
asset_datas = list(asset_datas)
#print(asset_datas[0])
asset_info = {k:asset_datas[0][k] for k in keys}
asset_info['assets'] = [{k:v for k, v in a.items() if k not in keys+['operation']} for a in asset_datas]
new_cache.append(asset_info)
return new_cache
def diff(self, new_cache):
"""Compare the library cache with it current state and return the cache differential"""
cache = self.read()
cache_dict = {f"{a['filepath']}/{a['name']}": a for a in cache.asset_caches}
new_cache_dict = {f"{a['filepath']}/{a['name']}" : a for a in new_cache.asset_caches}
assets_added = [AssetCacheDiff(v, 'ADD') for k, v in new_cache.items() if k not in cache]
assets_removed = [AssetCacheDiff(v, 'REMOVED') for k, v in cache.items() if k not in new_cache]
assets_modified = [AssetCacheDiff(v, 'MODIFIED') for k, v in cache.items() if v not in assets_removed and v!= new_cache[k]]
if assets_added:
print(f'{len(assets_added)} Assets Added \n{tuple(a.name for a in assets_added[:10])}...\n')
if assets_removed:
print(f'{len(assets_removed)} Assets Removed \n{tuple(a.name for a in assets_removed[:10])}...\n')
if assets_modified:
print(f'{len(assets_modified)} Assets Modified \n{tuple(a.name for a in assets_modified[:10])}...\n')
cache_diff = LibraryCacheDiff()
cache_diff.set(assets_added+assets_removed+assets_modified)
if not len(LibraryCacheDiff):
print('No change in the library')
return cache_diff
def __len__(self):
return len(self._data)
def __iter__(self):
return iter(self._data)
def __getitem__(self, key):
return self._data[key]
def __str__(self):
return f'LibraryCache(library={self.library.name})'
print()
prefs = bpy.context.preferences.addons['asset_library'].preferences
library = prefs.env_libraries[0]
library_cache = LibraryCache.from_library(library).read()
data = library.library_type.fetch()
print(data)
print(library_cache[0][0])
#library_cache.diff(library.library_type.fetch())
#print(library_cache[0])

125
ui.py Normal file
View File

@ -0,0 +1,125 @@
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):
lib = get_active_library()
if not lib:
FILEBROWSER_HT_header._draw_asset_browser_buttons(self, context)
return
space_data = context.space_data
params = context.space_data.params
row = self.layout.row(align=True)
row.separator()
row.operator("assetlibrary.bundle", icon='UV_SYNC_SELECT', text='').name = lib.name
#op
#op.clean = False
#op.only_recent = True
lib.plugin.draw_header(row)
if context.selected_files and context.active_file:
row.separator()
row.label(text=context.active_file.name)
row.separator_spacer()
sub = row.row()
sub.ui_units_x = 10
sub.prop(params, "filter_search", text="", icon='VIEWZOOM')
row.separator_spacer()
row.prop_with_popover(
params,
"display_type",
panel="ASSETBROWSER_PT_display",
text="",
icon_only=True,
)
row.operator(
"screen.region_toggle",
text="",
icon='PREFERENCES',
depress=is_option_region_visible(context, space_data)
).region_type = 'TOOL_PROPS'
def draw_assetbrowser_asset_menu(self, context):
layout = self.layout
layout.operator("assetlibrary.publish_assets", text='Publish Assets', icon='ASSET_MANAGER')
# 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,
# # ASSETLIB_PT_pose_library_usage,
# # ASSETLIB_MT_context_menu,
# # ASSETLIB_PT_libraries
# )
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 bl_class in bl_classes:
bpy.utils.register_class(bl_class)
#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 bl_class in reversed(bl_classes):
bpy.utils.unregister_class(bl_class)
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)