start refacto

DEV
christophe.seux 2024-05-27 17:22:45 +02:00
parent f7c125ae7b
commit a01b282f45
61 changed files with 528 additions and 3243 deletions

View File

@ -8,96 +8,41 @@ Extending features of the Asset Browser for a studio use.
bl_info = { bl_info = {
"name": "Asset Library", "name": "Asset Library",
"description": "Asset Library based on the Asset Browser.", "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), "version": (2, 0),
"blender": (3, 3, 0), "blender": (4, 0, 2),
"warning": "In development, things may change", "warning": "In development, things may change",
"location": "Asset Browser -> Animations, and 3D Viewport -> Animation panel", "location": "Asset Browser",
"category": "Animation", "category": "Import-Export",
} }
#from typing import List, Tuple
from . import operators, properties, ui, preferences
from asset_library import pose modules = (
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, operators,
pose, properties,
action, ui,
collection,
file,
keymaps,
gui,
preferences preferences
) )
# Reload Modules from inside Blender
def load_handler(): if "bpy" in locals():
print('load_handler') import importlib
set_env_libraries()
bpy.ops.assetlib.set_paths(all=True)
if not bpy.app.background:
bpy.ops.assetlib.bundle(blocking=False, mode='AUTO_BUNDLE')
def register() -> None:
for mod in modules:
importlib.reload(mod)
for m in bl_modules:
m.register()
#prefs = get_addon_prefs() def register():
"""Register the addon Asset Library for Blender"""
for mod in modules:
mod.register()
def unregister():
"""Unregister the addon Asset Library for Blender"""
bpy.app.timers.register(load_handler, first_interval=1) for mod in reversed(modules):
mod.unregister()
def unregister() -> None:
prefs = get_addon_prefs()
bpy.utils.previews.remove(prefs.previews)
for m in reversed(bl_modules):
m.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

@ -14,11 +14,9 @@ ASSETLIB_FILENAME = "blender_assets.libs.json"
MODULE_DIR = Path(__file__).parent MODULE_DIR = Path(__file__).parent
RESOURCES_DIR = MODULE_DIR / 'resources' RESOURCES_DIR = MODULE_DIR / 'resources'
LIBRARY_TYPE_DIR = MODULE_DIR / 'library_types' PLUGINS_DIR = MODULE_DIR / 'plugins'
LIBRARY_TYPES = [] PLUGINS = set()
PLUGINS_ITEMS = [('NONE', 'None', '', 0)]
ADAPTER_DIR = MODULE_DIR / 'adapters'
ADAPTERS = []
PREVIEW_ASSETS_SCRIPT = MODULE_DIR / 'common' / 'preview_assets.py' PREVIEW_ASSETS_SCRIPT = MODULE_DIR / 'common' / 'preview_assets.py'

View File

@ -0,0 +1,54 @@
"""
Util function for this addon
"""
import bpy
from . bl_utils import get_addon_prefs
def thumbnail_blend_file(input_blend, output_img):
input_blend = Path(input_blend).resolve()
output_img = Path(output_img).resolve()
print(f'Thumbnailing {input_blend} to {output_img}')
blender_thumbnailer = Path(bpy.app.binary_path).parent / 'blender-thumbnailer'
output_img.parent.mkdir(exist_ok=True, parents=True)
subprocess.call([blender_thumbnailer, str(input_blend), str(output_img)])
success = output_img.exists()
if not success:
empty_preview = RESOURCES_DIR / 'empty_preview.png'
shutil.copy(str(empty_preview), str(output_img))
return success
def get_active_library():
'''Get the pref library properties from the active library of the asset browser'''
prefs = get_addon_prefs()
lib_ref = bpy.context.space_data.params.asset_library_reference
#Check for merged library
for l in prefs.libraries:
if l.name == lib_ref:
return l
def update_library_path(self, context):
"""Removing all asset libraries and recreate them"""
addon_prefs = get_addon_prefs()
libs = context.preferences.filepaths.asset_libraries
for i, lib in reversed(list(enumerate(libs))):
if (addon_lib := addon_prefs.libraries.get(lib.name)) and addon_lib.path == lib.path:
bpy.ops.preferences.asset_library_remove(index=i)
for addon_lib in addon_prefs.libraries:
if not addon_lib.use:
continue
bpy.ops.preferences.asset_library_add(directory=str(addon_lib.path))
libs[-1].name = addon_lib.name

View File

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

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

View File

@ -1,344 +1,76 @@
from typing import Set
#import shutil
from pathlib import Path
import subprocess
import importlib import importlib
import time
import json
import bpy import bpy
from bpy_extras import asset_utils
from bpy.types import Context, Operator
from bpy.props import (
BoolProperty,
EnumProperty,
StringProperty,
IntProperty)
#from asset_library.constants import (DATA_TYPES, DATA_TYPE_ITEMS, MODULE_DIR) from bpy.types import Operator
import asset_library from bpy.props import (BoolProperty, EnumProperty, StringProperty, IntProperty)
from asset_library.common.bl_utils import ( from .core.bl_utils import get_addon_prefs, unique_name
attr_set,
get_addon_prefs,
get_bl_cmd,
get_view3d_persp,
#suitable_areas,
refresh_asset_browsers,
load_datablocks)
from asset_library.common.file_utils import open_blender_file, synchronize
from asset_library.common.functions import get_active_library, asset_warning_callback
from textwrap import dedent
from tempfile import gettempdir
import gpu
from gpu_extras.batch import batch_for_shader
import blf
import bgl
class ASSETLIB_OT_remove_assets(Operator): class ASSETLIB_OT_reload_addon(Operator):
bl_idname = "assetlib.remove_assets" bl_idname = "assetlibrary.reload_addon"
bl_options = {"REGISTER", "UNDO", "INTERNAL"} bl_options = {"UNDO"}
bl_label = 'Remove Assets' bl_label = 'Reload Asset Library Addon'
bl_description = 'Remove Selected Assets' bl_description = 'Reload The Asset Library Addon and the addapters'
@classmethod def execute(self, context):
def poll(cls, context):
if not asset_utils.SpaceAssetInfo.is_asset_browser(context.space_data):
return False
sp = context.space_data print('Execute reload', __package__)
if sp.params.asset_library_ref == 'LOCAL':
return False
return True addon = importlib.import_module(__package__)
def execute(self, context: Context) -> Set[str]: addon.unregister()
asset = context.active_file importlib.reload(addon)
for mod in addon.modules:
lib = get_active_library() importlib.reload(mod)
lib_type = lib.library_type addon.register()
catalog = lib.read_catalog()
if not catalog.context.item:
self.report({'ERROR'}, 'The active asset is not in the catalog')
return {'CANCELLED'}
asset_name = context.asset_file_handle.name
asset_path = lib_type.format_path(asset.asset_data['filepath'])
asset_catalog = catalog.context.path
img_path = lib_type.get_image_path(name=asset_name, catalog=asset_catalog, filepath=asset_path)
video_path = lib_type.get_video_path(name=asset_name, catalog=asset_catalog, filepath=asset_path)
if asset_path and asset_path.exists():
asset_path.unlink()
if img_path and img_path.exists():
img_path.unlink()
if video_path and video_path.exists():
video_path.unlink()
#open_blender_file(filepath)
try:
asset_path.parent.rmdir()
except Exception:#Directory not empty
pass
bpy.ops.assetlib.bundle(name=lib.name, blocking=True)
return {'FINISHED'} return {'FINISHED'}
class ASSETLIB_OT_edit_data(Operator): class ASSETLIB_OT_remove_library(Operator):
bl_idname = "assetlib.edit_data" bl_idname = "assetlibrary.remove_library"
bl_label = "Edit Asset Data"
bl_description = "Edit Current Asset Data"
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
bl_label = 'Remove Library'
warning: StringProperty(name='') bl_description = 'Remove Library'
path: StringProperty(name='Path')
catalog: StringProperty(name='Catalog', update=asset_warning_callback, options={'TEXTEDIT_UPDATE'})
name: StringProperty(name='Name', update=asset_warning_callback, options={'TEXTEDIT_UPDATE'})
tags: StringProperty(name='Tags', description='Tags need to separate with a comma (,)')
description: StringProperty(name='Description')
@classmethod
def poll(cls, context):
if not asset_utils.SpaceAssetInfo.is_asset_browser(context.space_data):
return False
return True
def execute(self, context: Context) -> Set[str]:
prefs = get_addon_prefs()
lib = get_active_library()
if lib.merge_libraries:
lib = prefs.libraries[lib.store_library]
new_name = lib.library_type.norm_file_name(self.name)
new_asset_path = lib.library_type.get_asset_path(name=new_name, catalog=self.catalog)
#asset_data = lib.library_type.get_asset_data(self.asset)
asset_data = dict(
tags=[t.strip() for t in self.tags.split(',') if t],
description=self.description,
)
#lib.library_type.set_asset_catalog(asset, asset_data, catalog_data)
self.asset.name = self.name
lib.library_type.set_asset_tags(self.asset, asset_data)
lib.library_type.set_asset_info(self.asset, asset_data)
self.old_asset_path.unlink()
lib.library_type.write_asset(asset=self.asset, asset_path=new_asset_path)
if self.old_image_path.exists():
new_img_path = lib.library_type.get_image_path(new_name, self.catalog, new_asset_path)
self.old_image_path.rename(new_img_path)
if self.old_video_path.exists():
new_video_path = lib.library_type.get_video_path(new_name, self.catalog, new_asset_path)
self.old_video_path.rename(new_video_path)
#if self.old_description_path.exists():
# self.old_description_path.unlink()
try:
self.old_asset_path.parent.rmdir()
except Exception: #The folder is not empty
pass
diff_path = Path(bpy.app.tempdir, 'diff.json')
diff = [dict(name=self.old_asset_name, catalog=self.old_catalog, filepath=str(self.old_asset_path), operation='REMOVE')]
asset_data = lib.library_type.get_asset_data(self.asset)
diff += [dict(asset_data,
image=str(new_img_path),
filepath=str(new_asset_path),
type=lib.data_type,
library_id=lib.id,
catalog=self.catalog,
operation='ADD'
)]
print(diff)
diff_path.write_text(json.dumps(diff, indent=4), encoding='utf-8')
bpy.ops.assetlib.bundle(name=lib.name, diff=str(diff_path), blocking=True)
return {"FINISHED"}
def draw(self, context):
layout = self.layout
layout.separator()
layout.use_property_split = True
lib = get_active_library()
if lib.merge_libraries:
layout.prop(lib, 'store_library', expand=False)
layout.prop(self, "catalog", text="Catalog")
layout.prop(self, "name", text="Name")
layout.prop(self, 'tags')
layout.prop(self, 'description')
#layout.prop()
layout.separator()
col = layout.column()
col.use_property_split = False
#row.enabled = False
if self.path:
col.label(text=self.path)
if self.warning:
col.label(icon='ERROR', text=self.warning)
def invoke(self, context, event):
lib = get_active_library()
active_lib = lib.library_type.get_active_asset_library()
lib.store_library = active_lib.name
asset_handle = context.asset_file_handle
catalog_file = lib.library_type.read_catalog()
catalog_ids = {v['id']: {'path': k, 'name': v['name']} for k,v in catalog_file.items()}
#asset_handle = context.asset_file_handle
self.old_asset_name = asset_handle.name
self.old_asset_path = lib.library_type.get_active_asset_path()
self.asset = load_datablocks(self.old_asset_path, self.old_asset_name, type=lib.data_types)
if not self.asset:
self.report({'ERROR'}, 'No asset found')
self.name = self.old_asset_name
self.description = asset_handle.asset_data.description
tags = [t.strip() for t in self.asset.asset_data.tags.keys() if t]
self.tags = ', '.join(tags)
#asset_path
self.old_catalog = catalog_ids[asset_handle.asset_data.catalog_id]['path']
self.catalog = self.old_catalog
self.old_image_path = lib.library_type.get_image_path(name=self.name, catalog=self.catalog, filepath=self.old_asset_path)
self.old_video_path = lib.library_type.get_video_path(name=self.name, catalog=self.catalog, filepath=self.old_asset_path)
#self.old_description_path = lib.library_type.get_description_path(self.old_asset_path)
#self.old_asset_info = lib.library_type.read_asset_info_file(self.old_asset_path)
#self.old_asset_info = lib.library_type.norm_asset_datas([self.old_asset_info])[0]
return context.window_manager.invoke_props_dialog(self, width=450)
def cancel(self, context):
print('Cancel Edit Data, removing the asset')
lib = get_active_library()
active_lib = lib.library_type.get_active_asset_library()
getattr(bpy.data, active_lib.data_types).remove(self.asset)
class ASSETLIB_OT_remove_user_library(Operator):
bl_idname = "assetlib.remove_user_library"
bl_options = {"REGISTER", "UNDO"}
bl_label = 'Remove User Library'
bl_description = 'Remove User Library'
index : IntProperty(default=-1) index : IntProperty(default=-1)
def execute(self, context: Context) -> Set[str]: def execute(self, context):
prefs = get_addon_prefs() prefs = get_addon_prefs()
prefs.user_libraries.remove(self.index) addon_lib = prefs.libraries[self.index]
bl_libs = context.preferences.filepaths.asset_libraries
if (bl_lib := bl_libs.get(addon_lib.name)) and bl_lib.path == addon_lib.path:
index = list(bl_libs).index(bl_lib)
bpy.ops.preferences.asset_library_remove(index=index)
prefs.libraries.remove(self.index)
return {'FINISHED'} return {'FINISHED'}
class ASSETLIB_OT_add_user_library(Operator): class ASSETLIB_OT_add_library(Operator):
bl_idname = "assetlib.add_user_library" bl_idname = "assetlibrary.add_library"
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
bl_label = 'Add User Library' bl_label = 'Add Library'
bl_description = 'Add User Library' bl_description = 'Add Library'
def execute(self, context):
def execute(self, context: Context) -> Set[str]:
prefs = get_addon_prefs() prefs = get_addon_prefs()
lib = prefs.user_libraries.add() lib = prefs.libraries.add()
lib.expand = True lib.expand = True
lib.name = unique_name('Asset Library', [l.name for l in prefs.libraries])
return {'FINISHED'} return {'FINISHED'}
class ASSETLIB_OT_open_blend(Operator): class ASSETLIB_OT_synchronize(Operator):
bl_idname = "assetlib.open_blend" bl_idname = "assetlibrary.synchronize"
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
bl_label = 'Open Blender File' bl_label = 'Synchronize'
bl_description = 'Open blender file' bl_description = 'Synchronize Action Lib to Local Directory'
#filepath : StringProperty(subtype='FILE_PATH')
def execute(self, context: Context) -> Set[str]:
#asset = context.active_file
#prefs = get_addon_prefs()
lib = get_active_library()
#filepath = lib.library_type.format_path(asset.asset_data['filepath'])
filepath = lib.library_type.get_active_asset_path()
open_blender_file(filepath)
return {'FINISHED'}
class ASSETLIB_OT_set_paths(Operator):
bl_idname = "assetlib.set_paths"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
bl_label = 'Set Paths'
bl_description = 'Set Library Paths'
name: StringProperty()
all: BoolProperty(default=False)
def execute(self, context: Context) -> Set[str]:
prefs = get_addon_prefs()
print('Set Paths')
if self.all:
libs = prefs.libraries
else:
libs = [prefs.libraries[self.name]]
for lib in libs:
lib.clear_library_path()
lib.set_library_path()
return {'FINISHED'}
class ASSETLIB_OT_bundle_library(Operator):
bl_idname = "assetlib.bundle"
bl_options = {"INTERNAL"}
bl_label = 'Bundle Library'
bl_description = 'Bundle all matching asset found inside one blend'
name : StringProperty() name : StringProperty()
diff : StringProperty() diff : StringProperty()
@ -351,7 +83,7 @@ class ASSETLIB_OT_bundle_library(Operator):
# bpy.ops.asset.library_refresh({"area": area, 'region': area.regions[3]}) # bpy.ops.asset.library_refresh({"area": area, 'region': area.regions[3]})
#space_data.activate_asset_by_id(asset, deferred=deferred) #space_data.activate_asset_by_id(asset, deferred=deferred)
def execute(self, context: Context) -> Set[str]: def execute(self, context):
prefs = get_addon_prefs() prefs = get_addon_prefs()
libs = [] libs = []
@ -377,7 +109,7 @@ class ASSETLIB_OT_bundle_library(Operator):
for lib_data in {lib_datas}: for lib_data in {lib_datas}:
lib = prefs.env_libraries.add() lib = prefs.env_libraries.add()
lib.set_dict(lib_data) lib.set_dict(lib_data)
lib.library_type.bundle(cache_diff='{self.diff}') lib.plugin.bundle(cache_diff='{self.diff}')
bpy.ops.wm.quit_blender() bpy.ops.wm.quit_blender()
""") """)
@ -401,437 +133,19 @@ class ASSETLIB_OT_bundle_library(Operator):
return {'FINISHED'} return {'FINISHED'}
class ASSETLIB_OT_reload_addon(Operator):
bl_idname = "assetlib.reload_addon"
bl_options = {"UNDO"}
bl_label = 'Reload Asset Library Addon'
bl_description = 'Reload The Asset Library Addon and the addapters'
def execute(self, context: Context) -> Set[str]:
print('Execute reload')
asset_library.unregister()
importlib.reload(asset_library)
asset_library.register()
return {'FINISHED'}
class ASSETLIB_OT_diff(Operator):
bl_idname = "assetlib.diff"
bl_options = {"REGISTER", "UNDO"}
bl_label = 'Synchronize'
bl_description = 'Synchronize Action Lib to Local Directory'
name : StringProperty()
conform : BoolProperty(default=False)
def execute(self, context: Context) -> Set[str]:
prefs = get_addon_prefs()
lib = prefs.libraries.get(self.name)
lib.library_type.diff()
return {'FINISHED'}
'''
class ASSETLIB_OT_conform_library(Operator):
bl_idname = "assetlib.conform_library"
bl_options = {"REGISTER", "UNDO"}
bl_label = "Conform Library"
bl_description = "Split each assets per blend and externalize preview"
name : StringProperty()
template_image : StringProperty()
template_video : StringProperty()
directory : StringProperty(subtype='DIR_PATH', name='Filepath')
def execute(self, context: Context) -> Set[str]:
prefs = get_addon_prefs()
lib = prefs.libraries.get(self.name)
#lib.library_type.conform(self.directory)
templates = {}
if self.template_image:
templates['image'] = self.template_image
if self.template_video:
templates['video'] = self.template_video
script_path = Path(bpy.app.tempdir) / 'bundle_library.py'
script_code = dedent(f"""
import bpy
prefs = bpy.context.preferences.addons["asset_library"].preferences
lib = prefs.env_libraries.add()
lib.set_dict({lib.to_dict()})
lib.library_type.conform(directory='{self.directory}', templates={templates})
""")
script_path.write_text(script_code)
cmd = get_bl_cmd(script=str(script_path), background=True)
subprocess.Popen(cmd)
return {'FINISHED'}
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {'RUNNING_MODAL'}
'''
class ASSETLIB_OT_make_custom_preview(Operator):
bl_idname = "assetlib.make_custom_preview"
bl_label = "Custom Preview"
bl_description = "Set a camera to preview an asset"
image_size : IntProperty(default=512)
modal : BoolProperty(default=False)
def modal(self, context, event):
if event.type in {'ESC'}: # Cancel
self.restore()
return {'CANCELLED'}
elif event.type in {'RET', 'NUMPAD_ENTER'}: # Cancel
return self.execute(context)
#return {'FINISHED'}
return {'PASS_THROUGH'}
def execute(self, context):
prefs = get_addon_prefs()
bpy.ops.render.opengl(write_still=True)
img_path = context.scene.render.filepath
#print('Load Image to previews')
prefs.previews.load(Path(img_path).stem, img_path, 'IMAGE')
#img = bpy.data.images.load(context.scene.render.filepath)
#img.update()
#img.preview_ensure()
#Copy the image with a new name
# render = bpy.data.images['Render Result']
# render_pixels = [0] * self.image_size * self.image_size * 4
# render.pixels.foreach_get(render_pixels)
# img = bpy.data.images.new(name=img_name, width=self.image_size, height=self.image_size, is_data=True, alpha=True)
# img.pixels.foreach_set(render_pixels)
#img.scale(128, 128)
#img.preview_ensure()
# preview_size = render.size
# pixels = [0] * preview_size[0] * preview_size[1] * 4
# render.pixels.foreach_get(pixels)
# image.preview.image_size = preview_size
# image.preview.image_pixels_float.foreach_set(pixels)
self.restore()
#self.is_running = False
prefs.preview_modal = False
return {"FINISHED"}
def restore(self):
print('RESTORE')
try:
bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW')
except:
print('Failed remove handler')
pass
bpy.data.objects.remove(self.camera)
self.attr_changed.restore()
def draw_callback_px(self, context):
if context.space_data != self._space_data:
return
dpi = context.preferences.system.dpi
bg_color = (0.8, 0.1, 0.1, 0.5)
font_color = (1, 1, 1, 1)
text = f'Escape: Cancel Enter: Make Preview'
font_id = 0
dim = blf.dimensions(font_id, text)
#gpu.state.line_width_set(100)
# bgl.glLineWidth(100)
# self.shader_2d.bind()
# self.shader_2d.uniform_float("color", bg_color)
# self.screen_framing.draw(self.shader_2d)
# # Reset
# gpu.state.line_width_set(1)
# -dim[0]/2, +dim[1]/2 + 5
# Display Text
blf.color(font_id, *font_color) # unpack color
blf.position(font_id, context.region.width/2 -dim[0]/2, dim[1]/2 + 5, 0)
blf.size(font_id, 12, dpi)
blf.draw(font_id, f'Escape: Cancel Enter: Make Preview')
def get_image_name(self):
prefs = get_addon_prefs()
preview_names = [p for p in prefs.previews.keys()]
preview_names.sort()
index = 0
if preview_names:
index = int(preview_names[-1][-2:]) + 1
return f'preview_{index:03d}'
def invoke(self, context, event):
prefs = get_addon_prefs()
cam_data = bpy.data.cameras.new(name='Preview Camera')
self.camera = bpy.data.objects.new(name='Preview Camera', object_data=cam_data)
#view_3d = get_view3d_persp()
scn = context.scene
space = context.space_data
matrix = space.region_3d.view_matrix.inverted()
if space.region_3d.view_perspective == 'CAMERA':
matrix = scn.camera.matrix_world
self.camera.matrix_world = matrix
img_name = self.get_image_name()
img_path = Path(bpy.app.tempdir, img_name).with_suffix('.webp')
self.attr_changed = attr_set([
(space.overlay, 'show_overlays', False),
(space.region_3d, 'view_perspective', 'CAMERA'),
(space.region_3d, 'view_camera_offset'),
(space.region_3d, 'view_camera_zoom'),
(space, 'lock_camera', True),
(space, 'show_region_ui', False),
(scn, 'camera', self.camera),
(scn.render, 'resolution_percentage', 100),
(scn.render, 'resolution_x', self.image_size),
(scn.render, 'resolution_y', self.image_size),
(scn.render, 'film_transparent', True),
(scn.render.image_settings, 'file_format', 'WEBP'),
(scn.render.image_settings, 'color_mode', 'RGBA'),
#(scn.render.image_settings, 'color_depth', '8'),
(scn.render, 'use_overwrite', True),
(scn.render, 'filepath', str(img_path)),
])
bpy.ops.view3d.view_center_camera()
space.region_3d.view_camera_zoom -= 6
space.region_3d.view_camera_offset[1] += 0.03
w, h = (context.region.width, context.region.height)
self._space_data = context.space_data
if self.modal:
prefs.preview_modal = True
self.shader_2d = gpu.shader.from_builtin('2D_UNIFORM_COLOR')
self.screen_framing = batch_for_shader(
self.shader_2d, 'LINE_LOOP', {"pos": [(0,0), (0,h), (w,h), (w,0)]})
self._handle = bpy.types.SpaceView3D.draw_handler_add(self.draw_callback_px, (context,), 'WINDOW', 'POST_PIXEL')
context.window_manager.modal_handler_add(self)
return {'RUNNING_MODAL'}
else:
return self.execute(context)
class ASSETLIB_OT_generate_previews(Operator):
bl_idname = "assetlib.generate_previews"
bl_options = {"REGISTER", "UNDO"}
bl_label = "Generate Previews"
bl_description = "Generate and write the image for assets"
cache : StringProperty()
preview_blend : StringProperty()
name : StringProperty()
blocking : BoolProperty(default=True)
def execute(self, context: Context) -> Set[str]:
prefs = get_addon_prefs()
lib = prefs.libraries.get(self.name)
# self.write_file(self.diff_file, self.diff)
# preview_assets = [(a.asset_data['filepath'], self.data_types, a.name) for a in assets]
# self.preview_assets_file.write_text(json.dumps(preview_assets), encoding='utf-8')
# cmd = [
# bpy.app.binary_path, '-b', '--use-system-env',
# '--python', str(PREVIEW_ASSETS_SCRIPT), '--',
# '--preview-blend', str(self.preview_blend),
# '--preview-assets-file', str(self.preview_assets_file)
# ]
# subprocess.call(cmd)
preview_blend = self.preview_blend or lib.library_type.preview_blend
if not preview_blend or not Path(preview_blend).exists():
preview_blend = MODULE_DIR / 'common' / 'preview.blend'
script_path = Path(bpy.app.tempdir) / 'generate_previews.py'
script_code = dedent(f"""
import bpy
prefs = bpy.context.preferences.addons["asset_library"].preferences
lib = prefs.env_libraries.add()
lib.set_dict({lib.to_dict()})
bpy.ops.wm.open_mainfile(filepath='{preview_blend}', load_ui=True)
lib.library_type.generate_previews(cache='{self.cache}')
""")
script_path.write_text(script_code)
cmd = get_bl_cmd(script=str(script_path), background=True)
if self.blocking:
subprocess.call(cmd)
else:
subprocess.Popen(cmd)
return {'FINISHED'}
class ASSETLIB_OT_play_preview(Operator):
bl_idname = "assetlib.play_preview"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
bl_label = 'Play Preview'
bl_description = 'Play Preview'
@classmethod
def poll(cls, context: Context) -> bool:
if not context.active_file:
return False
if not asset_utils.SpaceAssetInfo.is_asset_browser(context.space_data):
cls.poll_message_set("Current editor is not an asset browser")
return False
lib = get_active_library()
if not lib:
return False
return True
def execute(self, context: Context) -> Set[str]:
asset = context.active_file
prefs = get_addon_prefs()
lib = get_active_library()
#filepath = lib.library_type.format_path(asset.asset_data['filepath'])
asset_path = lib.library_type.get_active_asset_path()
asset_image = lib.library_type.get_image(asset.name, asset_path)
asset_video = lib.library_type.get_video(asset.name, asset_path)
if not asset_image and not asset_video:
self.report({'ERROR'}, f'Preview for {asset.name} not found.')
return {"CANCELLED"}
if asset_video:
self.report({'INFO'}, f'Video found. {asset_video}.')
if prefs.video_player:
subprocess.Popen([prefs.video_player, asset_video])
else:
bpy.ops.wm.path_open(filepath=str(asset_video))
else:
self.report({'INFO'}, f'Image found. {asset_image}.')
if prefs.image_player:
subprocess.Popen([prefs.image_player, asset_image])
else:
bpy.ops.wm.path_open(filepath=str(asset_image))
return {"FINISHED"}
class ASSETLIB_OT_synchronize(Operator):
bl_idname = "assetlib.synchronize"
bl_options = {"REGISTER", "UNDO"}
bl_label = 'Synchronize'
bl_description = 'Synchronize Action Lib to Local Directory'
clean : BoolProperty(default=False)
only_new : BoolProperty(default=False)
only_recent : BoolProperty(default=False)
name: StringProperty()
all: BoolProperty(default=False)
def execute(self, context: Context) -> Set[str]:
print('Not yet Implemented, have to be replace by Bundle instead')
return {'FINISHED'}
prefs = get_addon_prefs()
print('Synchronize')
if self.all:
libs = prefs.libraries
else:
libs = [prefs.libraries.get(self.name)]
for lib in libs:
if self.clean and Path(lib.path_local).exists():
pass
print('To check first')
#shutil.rmtree(path_local)
if not lib.path_local:
continue
synchronize(
src=lib.path,
dst=lib.path_local,
only_new=self.only_new,
only_recent=self.only_recent
)
return {'FINISHED'}
classes = ( classes = (
ASSETLIB_OT_play_preview,
ASSETLIB_OT_open_blend,
ASSETLIB_OT_set_paths,
ASSETLIB_OT_synchronize,
ASSETLIB_OT_add_user_library,
ASSETLIB_OT_remove_user_library,
ASSETLIB_OT_diff,
ASSETLIB_OT_generate_previews,
ASSETLIB_OT_bundle_library,
ASSETLIB_OT_remove_assets,
ASSETLIB_OT_edit_data,
#ASSETLIB_OT_conform_library,
ASSETLIB_OT_reload_addon, ASSETLIB_OT_reload_addon,
ASSETLIB_OT_make_custom_preview ASSETLIB_OT_add_library,
ASSETLIB_OT_remove_library,
ASSETLIB_OT_synchronize
) )
def register():
#bpy.types.UserAssetLibrary.is_env = False
def register():
for cls in classes: for cls in classes:
bpy.utils.register_class(cls) bpy.utils.register_class(cls)
def unregister(): def unregister():
for cls in reversed(classes): for cls in reversed(classes):
bpy.utils.unregister_class(cls) bpy.utils.unregister_class(cls)

17
plugins/__init__.py Normal file
View File

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

View File

@ -4,9 +4,9 @@ Plugin for making an asset library of all blender file found in a folder
""" """
from asset_library.library_types.scan_folder import ScanFolder from .scan_folder import ScanFolder
from asset_library.common.bl_utils import load_datablocks from ..core.bl_utils import load_datablocks
from asset_library.common.template import Template from ..core.template import Template
import bpy import bpy
from bpy.props import (StringProperty, IntProperty, BoolProperty) 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 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 from os.path import expandvars
import bpy import bpy
from bpy.props import StringProperty
from .library_plugin import LibraryPlugin
from ..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""" """Copy library folder from a server to a local disk for better performance"""
name = "Copy Folder" name = "Copy Folder"

View File

@ -3,13 +3,6 @@
Plugin for making an asset library of all blender file found in a folder 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 import re
from pathlib import Path from pathlib import Path
from itertools import groupby from itertools import groupby
@ -21,8 +14,15 @@ import urllib3
import traceback import traceback
import time import time
import bpy
from bpy.props import (StringProperty, IntProperty, BoolProperty)
class Kitsu(LibraryType): from .library_plugin import LibraryPlugin
from ..core.template import Template
from ..core.file_utils import install_module
class Kitsu(LibraryPlugin):
name = "Kitsu" name = "Kitsu"
template_name : StringProperty() 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 os
import shutil
import json import json
import uuid import uuid
import time import time
from functools import partial
import subprocess import subprocess
from pathlib import Path
from itertools import groupby
from functools import partial
from glob import glob from glob import glob
from copy import deepcopy 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 ..core.bl_utils import get_addon_prefs, load_datablocks
from ..core.file_utils import read_file, write_file
from ..core.template import Template
from ..constants import (MODULE_DIR, RESOURCES_DIR)
from ..data_type import (action, collection, file)
#from asset_library.common.library_cache import LibraryCacheDiff
class LibraryPlugin(PropertyGroup):
#def __init__(self): #def __init__(self):
name = "Base Adapter" #name = "Base Adapter"
#library = None #library = None
@property @property
def library(self): def library(self):
prefs = self.addon_prefs prefs = self.addon_prefs
for lib in prefs.libraries: for lib in prefs.libraries:
if lib.library_type == self: if lib.plugin == self:
return lib return lib
@property @property
@ -104,7 +105,7 @@ class LibraryType(PropertyGroup):
return self.library.read_cache(filepath=filepath) return self.library.read_cache(filepath=filepath)
def fetch(self): 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): def norm_file_name(self, name):
return name.replace(' ', '_') return name.replace(' ', '_')
@ -186,7 +187,7 @@ class LibraryType(PropertyGroup):
if 'filepath' in asset_handle.asset_data: if 'filepath' in asset_handle.asset_data:
asset_path = asset_handle.asset_data['filepath'] 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: else:
asset_path = bpy.types.AssetHandle.get_full_library_path( asset_path = bpy.types.AssetHandle.get_full_library_path(
asset_handle, bpy.context.asset_library_ref asset_handle, bpy.context.asset_library_ref
@ -195,22 +196,22 @@ class LibraryType(PropertyGroup):
return asset_path return asset_path
def generate_previews(self): 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): 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): 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): 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): 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): 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): def format_asset_data(self, data):
"""Get a dict for use in template fields""" """Get a dict for use in template fields"""
@ -321,7 +322,7 @@ class LibraryType(PropertyGroup):
# return write_file(cache_path, list(asset_infos)) # return write_file(cache_path, list(asset_infos))
def prop_rel_path(self, path, prop): 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 field_prop = '{%s}/'%prop
prop_value = getattr(self, 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 # Write the cache in a temporary file for the generate preview script
tmp_cache_file = cache.write(tmp=True) 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)): elif isinstance(cache_diff, (Path, str)):
cache_diff = LibraryCacheDiff(cache_diff).read()#json.loads(Path(cache_diff).read_text(encoding='utf-8')) 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 # return list(new_cache.values()), cache_diff
def draw_prefs(self, layout): 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__ annotations = self.__class__.__annotations__
for k, v in annotations.items(): 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 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 os
import re
import uuid
import shutil import shutil
import json import json
import requests import requests
import urllib3 import urllib3
import traceback import traceback
import time import time
from itertools import groupby
from pathlib import Path
from pprint import pprint as pp from pprint import pprint as pp
import bpy
from bpy.props import (StringProperty, IntProperty, BoolProperty, EnumProperty)
from .library_plugin import LibraryPlugin
from ..core.template import Template
from ..core.file_utils import install_module
REQ_HEADERS = requests.utils.default_headers() REQ_HEADERS = requests.utils.default_headers()
REQ_HEADERS.update({"User-Agent": "Blender: PH Assets"}) REQ_HEADERS.update({"User-Agent": "Blender: PH Assets"})
class PolyHaven(LibraryType): class PolyHaven(LibraryPlugin):
name = "Poly Haven" name = "Poly Haven"
# template_name : StringProperty() # template_name : StringProperty()

View File

@ -3,24 +3,25 @@
Plugin for making an asset library of all blender file found in a folder 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 import re
from pathlib import Path
from itertools import groupby
import uuid import uuid
import os import os
import shutil import shutil
import json import json
import time import time
from pathlib import Path
from itertools import groupby
import bpy
from bpy.props import (StringProperty, IntProperty, BoolProperty)
from .library_plugin import LibraryPlugin
from ..core.bl_utils import load_datablocks
from ..core.template import Template
class ScanFolder(LibraryType):
class ScanFolder(LibraryPlugin):
name = "Scan Folder" name = "Scan Folder"
source_directory : StringProperty(subtype='DIR_PATH') 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)) return self.format_path(self.source_template_video, dict(name=name, catalog=catalog, filepath=filepath))
def new_asset(self, asset, asset_data): 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): 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): 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 # Write the cache in a temporary file for the generate preview script
tmp_cache_file = cache.write(tmp=True) 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)): elif isinstance(cache_diff, (Path, str)):
cache_diff = json.loads(Path(cache_diff).read_text(encoding='utf-8')) cache_diff = json.loads(Path(cache_diff).read_text(encoding='utf-8'))

View File

@ -1,774 +1,57 @@
import bpy
import os import bpy
from os.path import abspath, join from bpy.types import AddonPreferences
from bpy.props import (CollectionProperty, StringProperty)
from bpy.types import (AddonPreferences, PointerProperty, PropertyGroup)
from bpy.props import (BoolProperty, StringProperty, CollectionProperty, from . properties import AssetLibrary
EnumProperty, IntProperty) from . core.bl_utils import get_addon_prefs
from . core.asset_library_utils import update_library_path
from asset_library.constants import (DATA_TYPES, DATA_TYPE_ITEMS,
ICONS, RESOURCES_DIR, LIBRARY_TYPE_DIR, LIBRARY_TYPES, ADAPTERS)
class AssetLibraryPrefs(AddonPreferences):
from asset_library.common.file_utils import import_module_from_path, norm_str bl_idname = __package__
from asset_library.common.bl_utils import get_addon_prefs
from asset_library.common.library_cache import LibraryCache libraries : CollectionProperty(type=AssetLibrary)
from asset_library.common.catalog import Catalog bundle_directory : StringProperty(
#from asset_library.common.functions import get_catalog_path name="Path",
subtype='DIR_PATH',
from pathlib import Path default='',
import importlib update=update_library_path
import inspect )
def draw(self, context):
def update_library_config(self, context): prefs = get_addon_prefs()
print('update_library_config not yet implemented')
layout = self.layout
def update_library_path(self, context): col = layout.column(align=False)
prefs = get_addon_prefs()
col.prop(self, "bundle_directory", text='Bundle Directory')
self['bundle_directory'] = str(self.library_path) col.separator()
if not self.custom_bundle_name: row = col.row()
self['custom_bundle_name'] = self.name row.label(text='Libraries:')
#row.alignment = 'RIGHT'
if not self.custom_bundle_directory: row.separator_spacer()
custom_bundle_dir = Path(prefs.bundle_directory, self.library_name).resolve() row.operator("assetlibrary.reload_addon", icon='FILE_REFRESH', text='')
self['custom_bundle_directory'] = str(custom_bundle_dir) row.operator("assetlibrary.add_library", icon="ADD", text='', emboss=False)
#if self.custom_bundle_directory: for lib in self.libraries:# list(self.env_libraries) + list(self.user_libraries):
# self['custom_bundle_directory'] = abspath(bpy.path.abspath(self.custom_bundle_directory)) lib.draw(col)
#else:
# bundle_directory = join(prefs.bundle_directory, norm_str(self.name))
# self['custom_bundle_directory'] = abspath(bundle_directory)
self.set_library_path() classes = (
AssetLibraryPrefs,
def update_all_library_path(self, context): )
#print('update_all_assetlib_paths')
prefs = get_addon_prefs() def register():
for cls in classes:
#if self.custom_bundle_directory: bpy.utils.register_class(cls)
# self['custom_bundle_directory'] = abspath(bpy.path.abspath(self.custom_bundle_directory))
for lib in prefs.libraries: def unregister():
update_library_path(lib, context) for cls in reversed(classes):
#lib.set_library_path() bpy.utils.unregister_class(cls)
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()

134
properties.py Normal file
View File

@ -0,0 +1,134 @@
import inspect
import bpy
from bpy.types import (AddonPreferences, PropertyGroup)
from bpy.props import (BoolProperty, StringProperty, CollectionProperty,
EnumProperty, IntProperty, PointerProperty)
from .core.bl_utils import get_addon_prefs
from .core.file_utils import import_module_from_path, norm_str
from .core.asset_library_utils import update_library_path
from .constants import PLUGINS, PLUGINS_DIR, PLUGINS_ITEMS
def load_plugins():
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):
continue
if AssetLibrary not in obj.__mro__ or obj is AssetLibrary:
continue
try:
print(f'Register Plugin {name}')
bpy.utils.register_class(obj)
setattr(Plugins, norm_str(obj.name), PointerProperty(type=obj))
PLUGINS.append(obj)
except Exception as e:
print(f'Could not register plugin {name}')
print(e)
plugin_items = [('NONE', 'None', '', 0)]
plugin_items += [(norm_str(p.name, format=str.upper), p.name, "", i+1) for i, a in enumerate(PLUGINS)]
PLUGINS_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=update_library_path)
expand : BoolProperty(name='Expand', default=False)
use : BoolProperty(name='Use', default=True, update=update_library_path)
is_env : BoolProperty(default=False)
#path : StringProperty(subtype='DIR_PATH')
plugins : PointerProperty(type=Plugins)
plugin_name : EnumProperty(items=lambda s, c : PLUGINS_ITEMS)
@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 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)
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")
classes = (
Plugins,
AssetLibrary,
)
def register():
for cls in classes:
bpy.utils.register_class(cls)
load_plugins()
def unregister():
for cls in reversed(classes):
bpy.utils.unregister_class(cls)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 731 B

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

86
ui.py Normal file
View File

@ -0,0 +1,86 @@
from bpy.types import FILEBROWSER_HT_header, ASSETBROWSER_MT_editor_menus
from .core.asset_library_utils import get_active_library
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_header(self, context):
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
# )
classes = []
def register() -> None:
for cls in classes:
bpy.utils.register_class(cls)
ASSETBROWSER_MT_editor_menus.append(draw_assetbrowser_header)
def unregister() -> None:
for cls in reversed(classes):
bpy.utils.unregister_class(cls)
ASSETBROWSER_MT_editor_menus.remove(draw_assetbrowser_header)