First Commit

This commit is contained in:
“christopheseux”
2022-12-24 15:30:32 +01:00
parent 3c7f133a85
commit e3d24aa4e3
49 changed files with 8466 additions and 4 deletions
+26
View File
@@ -0,0 +1,26 @@
#from asset_library.bundle_blend import bundle_blend, bundle_library
#from file_utils import (norm_str, norm_value,
# norm_arg, get_bl_cmd, copy_file, copy_dir)
#from asset_library.functions import
#from asset_library.common import bundle_blend
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
if 'bpy' in locals():
import importlib
#importlib.reload(bundle_blend)
importlib.reload(file_utils)
importlib.reload(functions)
importlib.reload(synchronize)
importlib.reload(template)
import bpy
+465
View File
@@ -0,0 +1,465 @@
"""
Generic Blender functions
"""
from pathlib import Path
from fnmatch import fnmatch
from typing import Any, List, Iterable, Optional, Tuple
Datablock = Any
import bpy
from bpy_extras import asset_utils
from asset_library.constants import RESOURCES_DIR
#from asset_library.common.file_utils import no
from os.path import abspath
class attr_set():
'''Receive a list of tuple [(data_path, "attribute" [, wanted value)] ]
entering with-statement : Store existing values, assign wanted value (if any)
exiting with-statement: Restore values to their old values
'''
def __init__(self, attrib_list):
self.store = []
# item = (prop, attr, [new_val])
for item in attrib_list:
prop, attr = item[:2]
self.store.append( (prop, attr, getattr(prop, attr)) )
if len(item) >= 3:
try:
setattr(prop, attr, item[2])
except TypeError:
print(f'Cannot set attribute {attr} to {prop}')
def __enter__(self):
return self
def __exit__(self, exc_type, exc_value, exc_traceback):
for prop, attr, old_val in self.store:
setattr(prop, attr, old_val)
def get_overriden_col(ob, scene=None):
scn = scene or bpy.context.scene
cols = [c for c in bpy.data.collections if scn.user_of_id(c)]
return next((c for c in cols if ob in c.all_objects[:]
if all(not c.override_library for c in get_col_parents(c))), None)
def get_view3d_persp():
windows = bpy.context.window_manager.windows
view_3ds = [a for w in windows for a in w.screen.areas if a.type == 'VIEW_3D']
view_3d = next((a for a in view_3ds if a.spaces.active.region_3d.view_perspective == 'PERSP'), view_3ds[0])
return view_3d
def biggest_asset_browser_area(screen: bpy.types.Screen) -> Optional[bpy.types.Area]:
"""Return the asset browser Area that's largest on screen.
:param screen: context.window.screen
:return: the Area, or None if no Asset Browser area exists.
"""
def area_sorting_key(area: bpy.types.Area) -> Tuple[bool, int]:
"""Return area size in pixels."""
return (area.width * area.height)
areas = list(suitable_areas(screen))
if not areas:
return None
return max(areas, key=area_sorting_key)
def suitable_areas(screen: bpy.types.Screen) -> Iterable[bpy.types.Area]:
"""Generator, yield Asset Browser areas."""
for area in screen.areas:
space_data = area.spaces[0]
if not asset_utils.SpaceAssetInfo.is_asset_browser(space_data):
continue
yield area
def area_from_context(context: bpy.types.Context) -> Optional[bpy.types.Area]:
"""Return an Asset Browser suitable for the given category.
Prefers the current Asset Browser if available, otherwise the biggest.
"""
space_data = context.space_data
if asset_utils.SpaceAssetInfo.is_asset_browser(space_data):
return context.area
# Try the current screen first.
browser_area = biggest_asset_browser_area(context.screen)
if browser_area:
return browser_area
for win in context.window_manager.windows:
if win.screen == context.screen:
continue
browser_area = biggest_asset_browser_area(win.screen)
if browser_area:
return browser_area
return None
def activate_asset(
asset: bpy.types.Action, asset_browser: bpy.types.Area, *, deferred: bool
) -> None:
"""Select & focus the asset in the browser."""
space_data = asset_browser.spaces[0]
assert asset_utils.SpaceAssetInfo.is_asset_browser(space_data)
space_data.activate_asset_by_id(asset, deferred=deferred)
def active_catalog_id(asset_browser: bpy.types.Area) -> str:
"""Return the ID of the catalog shown in the asset browser."""
return params(asset_browser).catalog_id
def get_asset_space_params(asset_browser: bpy.types.Area) -> bpy.types.FileAssetSelectParams:
"""Return the asset browser parameters given its Area."""
space_data = asset_browser.spaces[0]
assert asset_utils.SpaceAssetInfo.is_asset_browser(space_data)
return space_data.params
def refresh_asset_browsers():
for area in suitable_areas(bpy.context.screen):
bpy.ops.asset.library_refresh({"area": area, 'region': area.regions[3]})
def tag_redraw(screen: bpy.types.Screen) -> None:
"""Tag all asset browsers for redrawing."""
for area in suitable_areas(screen):
area.tag_redraw()
# def get_blender_command(file=None, script=None, background=True, **args):
# '''Return a Blender Command as a list to be used in a subprocess'''
# cmd = [bpy.app.binary_path]
# if file:
# cmd += [str(file)]
# if background:
# cmd += ['--background']
# if script:
# cmd += ['--python', str(script)]
# if args:
# cmd += ['--']
# for k, v in args.items():
# cmd += [f"--{k.replace('_', '-')}", str(v)]
# return cmd
def norm_value(value):
if isinstance(value, (tuple, list)):
values = []
for v in value:
if not isinstance(v, str):
v = json.dumps(v)
values.append(v)
return values
if isinstance(value, Path):
return str(value)
if not isinstance(value, str):
value = json.dumps(value)
return value
def norm_arg(arg_name, format=str.lower, prefix='--', separator='-'):
arg_name = norm_str(arg_name, format=format, separator=separator)
return prefix + arg_name
def get_bl_cmd(blender=None, background=False, focus=True, blendfile=None, script=None, **kargs):
cmd = [str(blender)] if blender else [bpy.app.binary_path]
if background:
cmd += ['--background']
if not focus and not background:
cmd += ['--no-window-focus']
cmd += ['--window-geometry', '5000', '0', '10', '10']
if blendfile:
cmd += [str(blendfile)]
if script:
cmd += ['--python', str(script)]
if kargs:
cmd += ['--']
for k, v in kargs.items():
k = norm_arg(k)
v = norm_value(v)
cmd += [k]
if isinstance(v, (tuple, list)):
cmd += v
else:
cmd += [v]
return cmd
def get_addon_prefs():
addon_name = __package__.split('.')[0]
return bpy.context.preferences.addons[addon_name].preferences
def thumbnail_blend_file(input_blend, output_img):
input_blend = Path(input_blend).resolve()
output_img = Path(output_img).resolve()
print(f'Thumbnailing {input_blend} to {output_img}')
blender_thumbnailer = Path(bpy.app.binary_path).parent / 'blender-thumbnailer'
output_img.parent.mkdir(exist_ok=True, parents=True)
subprocess.call([blender_thumbnailer, str(input_blend), str(output_img)])
success = output_img.exists()
if not success:
empty_preview = RESOURCES_DIR / 'empty_preview.png'
shutil.copy(str(empty_preview), str(output_img))
return success
def get_col_parents(col, root=None, cols=None):
'''Return a list of parents collections of passed col
root : Pass a collection to search in (recursive)
else search in master collection
'''
if cols is None:
cols = []
if root == None:
root = bpy.context.scene.collection
for sub in root.children:
if sub == col:
cols.append(root)
if len(sub.children):
cols = get_col_parents(col, root=sub, cols=cols)
return cols
def get_overriden_col(ob, scene=None):
'''Get the collection use for making the override'''
scn = scene or bpy.context.scene
cols = [c for c in bpy.data.collections if scn.user_of_id(c)]
return next((c for c in cols if ob in c.all_objects[:]
if all(not c.override_library for c in get_col_parents(c))), None)
def load_assets_from(filepath: Path) -> List[Datablock]:
if not has_assets(filepath):
# Avoid loading any datablocks when there are none marked as asset.
return []
# Append everything from the file.
with bpy.data.libraries.load(str(filepath)) as (
data_from,
data_to,
):
for attr in dir(data_to):
setattr(data_to, attr, getattr(data_from, attr))
# Iterate over the appended datablocks to find assets.
def loaded_datablocks() -> Iterable[Datablock]:
for attr in dir(data_to):
datablocks = getattr(data_to, attr)
for datablock in datablocks:
yield datablock
loaded_assets = []
for datablock in loaded_datablocks():
if not getattr(datablock, "asset_data", None):
continue
# Fake User is lost when appending from another file.
datablock.use_fake_user = True
loaded_assets.append(datablock)
return loaded_assets
def has_assets(filepath: Path) -> bool:
with bpy.data.libraries.load(str(filepath), assets_only=True) as (
data_from,
_,
):
for attr in dir(data_from):
data_names = getattr(data_from, attr)
if data_names:
return True
return False
def copy_frames(start, end, offset, path):
for i in range (start, end):
src = path.replace('####', f'{i:04d}')
dst = src.replace(src.split('_')[-1].split('.')[0], f'{i+offset:04d}')
shutil.copy2(src, dst)
def split_path(path) :
try :
bone_name = path.split('["')[1].split('"]')[0]
except :
bone_name = None
try :
prop_name = path.split('["')[2].split('"]')[0]
except :
prop_name = path.split('.')[-1]
return bone_name, prop_name
def load_datablocks(src, names=None, type='objects', link=True, expr=None) -> list:
return_list = not isinstance(names, str)
names = names or []
if not isinstance(names, (list, tuple)):
names = [names]
if isinstance(expr, str):
pattern = expr
expr = lambda x : fnmatch(x, pattern)
with bpy.data.libraries.load(str(src), link=link) as (data_from, data_to):
datablocks = getattr(data_from, type)
if expr:
names += [i for i in datablocks if expr(i)]
elif not names:
names = datablocks
setattr(data_to, type, names)
datablocks = getattr(data_to, type)
if return_list:
return datablocks
elif datablocks:
return datablocks[0]
"""
# --- Collection handling
"""
def col_as_asset(col, verbose=False):
if col is None:
return
if verbose:
print('linking:', col.name)
pcol = bpy.data.collections.new(col.name)
bpy.context.scene.collection.children.link(pcol)
pcol.children.link(col)
pcol.asset_mark()
return pcol
def load_col(filepath, name, link=True, override=True, rig_pattern=None, context=None):
'''Link a collection by name from a file and override if has armature'''
# with bpy.data.libraries.load(filepath, link=link) as (data_from, data_to):
# data_to.collections = [c for c in data_from.collections if c == name]
# if not data_to.collections:
# return
# return data_to.collections[0]
context = context or bpy.context
collections = load_datablocks(filepath, name, link=link, type='collections')
if not collections:
print(f'No collection "{name}" found in: {filepath}')
return
col = collections[0]
print('collection:', col.name)
## create instance object
inst = bpy.data.objects.new(col.name, None)
inst.instance_collection = col
inst.instance_type = 'COLLECTION'
context.scene.collection.objects.link(inst)
# make active
inst.select_set(True)
context.view_layer.objects.active = inst
## simple object (no armatures)
if not link or not override:
return inst
if not next((o for o in col.all_objects if o.type == 'ARMATURE'), None):
return inst
## Create the override
# Search
parent_cols = inst.users_collection
child_cols = [child for pcol in parent_cols for child in pcol.children]
params = {'active_object': inst, 'selected_objects': [inst]}
try:
bpy.ops.object.make_override_library(params)
## check which collection is new in parents collection
asset_col = next((c for pcol in parent_cols for c in pcol.children if c not in child_cols), None)
if not asset_col:
print('Overriden, but no collection found !!')
return
for ob in asset_col.all_objects:
if ob.type != 'ARMATURE':
continue
if rig_pattern and not fnmatch(ob.name, rig_pattern):
continue
ob.hide_select = ob.hide_viewport = False
ob.select_set(True)
context.view_layer.objects.active = ob
print(ob.name)
return ob
except Exception as e:
print(f'Override failed on {col.name}')
print(e)
return inst
def get_preview(asset_path='', asset_name=''):
asset_preview_dir = Path(asset_path).parents[1]
name = asset_name.lower()
return next((f for f in asset_preview_dir.rglob('*') if f.stem.lower().endswith(name)), None)
def get_object_libraries(ob):
if not ob :
return []
libraries = [ob.library, ob.data.library]
if ob.type in ('MESH', 'CURVE'):
libraries += [m.library for m in ob.data.materials if m]
filepaths = []
for l in libraries:
if not l or not l.filepath:
continue
absolute_filepath = abspath(bpy.path.abspath(l.filepath, library=l))
if absolute_filepath in filepaths:
continue
filepaths.append(absolute_filepath)
return filepaths
+307
View File
@@ -0,0 +1,307 @@
"""Generic python functions to make operation on file and names"""
import fnmatch
import json
import platform
import re
import subprocess
import unicodedata
import os
from pathlib import Path
import importlib
import sys
import shutil
def install_module(module_name, package_name=None):
'''Install a python module with pip or return it if already installed'''
try:
module = importlib.import_module(module_name)
except ModuleNotFoundError:
print(f'Installing Module {module_name} ....')
subprocess.call([sys.executable, '-m', 'ensurepip'])
subprocess.call([sys.executable, '-m', 'pip', 'install', package_name or module_name])
module = importlib.import_module(module_name)
return module
def import_module_from_path(path):
from importlib import util
try:
path = Path(path)
spec = util.spec_from_file_location(path.stem, str(path))
mod = util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod
except Exception as e:
print(f'Cannot import file {path}')
print(e)
def norm_str(string, separator='_', format=str.lower, padding=0):
string = str(string)
string = string.replace('_', ' ')
string = string.replace('-', ' ')
string = re.sub('[ ]+', ' ', string)
string = re.sub('[ ]+\/[ ]+', '/', string)
string = string.strip()
if format:
string = format(string)
# Padd rightest number
string = re.sub(r'(\d+)(?!.*\d)', lambda x : x.group(1).zfill(padding), string)
string = string.replace(' ', separator)
string = unicodedata.normalize('NFKD', string).encode('ASCII', 'ignore').decode("utf-8")
return string
def remove_version(filepath):
pattern = '_v[0-9]+\.'
search = re.search(pattern, filepath)
if search:
filepath = filepath.replace(search.group()[:-1], '')
return Path(filepath).name
def is_exclude(name, patterns) -> bool:
# from fnmatch import fnmatch
if not isinstance(patterns, (list,tuple)) :
patterns = [patterns]
return any([fnmatch(name, p) for p in patterns])
def get_last_files(root, pattern=r'_v\d{3}\.\w+', only_matching=False, ex_file=None, ex_dir=None, keep=1, verbose=False) -> list:
'''Recursively get last(s) file(s) (when there is multiple versions) in passed directory
root -> str: Filepath of the folder to scan.
pattern -> str: Regex pattern to group files.
only_matching -> bool: Discard files that aren't matched by regex pattern.
ex_file -> list : List of fn_match pattern to exclude files.
ex_dir -> list : List of fn_match pattern of directory name to skip.
keep -> int: Number of lasts versions to keep when there are mutliple versionned files (e.g: 1 keep only last).
verbose -> bool: Print infos in console.
'''
files = []
if ex_file is None:
all_items = [f for f in os.scandir(root)]
else:
all_items = [f for f in os.scandir(root) if not is_exclude(f.name, ex_file)]
allfiles = [f for f in all_items if f.is_file()]
# Need to sort to effectively group separated key in list
allfiles.sort(key=lambda x: x.name)
dirs = [f for f in all_items if f.is_dir()]
for i in range(len(allfiles)-1,-1,-1):# fastest way to iterate on index in reverse
if not re.search(pattern, allfiles[i].name):
if only_matching:
allfiles.pop(i)
else:
files.append(allfiles.pop(i).path)
# separate remaining files in prefix grouped lists
lilist = [list(v) for k, v in groupby(allfiles, key=lambda x: re.split(pattern, x.name)[0])]
# get only item last of each sorted grouplist
for l in lilist:
versions = sorted(l, key=lambda x: x.name)[-keep:] # exclude older
for f in versions:
files.append(f.path)
if verbose and len(l) > 1:
print(f'{root}: keep {str([x.name for x in versions])} out of {len(l)} elements')
for d in dirs: # recursively treat all detected directory
if ex_dir and is_exclude(d.name, ex_dir):
# skip folder with excluded name
continue
files += get_last_files(
d.path, pattern=pattern, only_matching=only_matching, ex_file=ex_file, ex_dir=ex_dir, keep=keep)
return sorted(files)
def copy_file(src, dst, only_new=False, only_recent=False):
if dst.exists():
if only_new:
return
elif only_recent and dst.stat().st_mtime >= src.stat().st_mtime:
return
dst.parent.mkdir(exist_ok=True, parents=True)
print(f'Copy file from {src} to {dst}')
if platform.system() == 'Windows':
subprocess.call(['copy', str(src), str(dst)], shell=True)
else:
subprocess.call(['cp', str(src), str(dst)])
def copy_dir(src, dst, only_new=False, only_recent=False, excludes=['.*'], includes=[]):
src, dst = Path(src), Path(dst)
if includes:
includes = r'|'.join([fnmatch.translate(x) for x in includes])
if excludes:
excludes = r'|'.join([fnmatch.translate(x) for x in excludes])
if dst.is_dir():
dst.mkdir(exist_ok=True, parents=True)
else:
dst.parent.mkdir(exist_ok=True, parents=True)
if src.is_file():
copy_file(src, dst, only_new=only_new, only_recent=only_recent)
elif src.is_dir():
src_files = list(src.rglob('*'))
if excludes:
src_files = [f for f in src_files if not re.match(excludes, f.name)]
if includes:
src_files = [f for f in src_files if re.match(includes, f.name)]
dst_files = [dst/f.relative_to(src) for f in src_files]
for src_file, dst_file in zip(src_files, dst_files) :
if src_file.is_dir():
dst_file.mkdir(exist_ok=True, parents=True)
else:
copy_file(src_file, dst_file, only_new=only_new, only_recent=only_recent)
def open_file(filepath, select=False):
'''Open a filepath inside the os explorer'''
if platform.system() == 'Darwin': # macOS
cmd = ['open']
elif platform.system() == 'Windows': # Windows
cmd = ['explorer']
if select:
cmd += ['/select,']
else: # linux variants
cmd = ['xdg-open']
if select:
cmd = ['nemo']
cmd += [str(filepath)]
subprocess.Popen(cmd)
def open_blender_file(filepath=None):
filepath = filepath or bpy.data.filepath
cmd = sys.argv
# if no filepath, use command as is to reopen blender
if filepath != '':
if len(cmd) > 1 and cmd[1].endswith('.blend'):
cmd[1] = str(filepath)
else:
cmd.insert(1, str(filepath))
subprocess.Popen(cmd)
def read_file(path):
'''Read a file with an extension in (json, yaml, yml, txt)'''
exts = ('.json', '.yaml', '.yml', '.txt')
if not path:
print('Try to read empty file')
path = Path(path)
if not path.exists():
print('File not exist', path)
return
if path.suffix not in exts:
print(f'Cannot read file {path}, extension must be in {exts}')
return
txt = path.read_text()
data = None
if path.suffix.lower() in ('.yaml', '.yml'):
yaml = install_module('yaml')
try:
data = yaml.safe_load(txt)
except Exception:
print(f'Could not load yaml file {path}')
return
elif path.suffix.lower() == '.json':
try:
data = json.loads(txt)
except Exception:
print(f'Could not load json file {path}')
return
else:
data = txt
return data
def write_file(path, data, indent=4):
'''Read a file with an extension in (json, yaml, yml, text)'''
exts = ('.json', '.yaml', '.yml', '.txt')
if not path:
print('Try to write empty file')
path = Path(path)
path.parent.mkdir(parents=True, exist_ok=True)
if path.suffix not in exts:
print(f'Cannot read file {path}, extension must be in {exts}')
return
if path.suffix.lower() in ('.yaml', '.yml'):
yaml = install_module('yaml')
try:
path.write_text(yaml.dump(data), encoding='utf8')
except Exception as e:
print(e)
print(f'Could not write yaml file {path}')
return
elif path.suffix.lower() == '.json':
try:
path.write_text(json.dumps(data, indent=indent), encoding='utf8')
except Exception as e:
print(e)
print(f'Could not write json file {path}')
return
else:
data = path.write_text(data, encoding='utf8')
def synchronize(src, dst, only_new=False, only_recent=False, clear=False):
#actionlib_dir = get_actionlib_dir(custom=custom)
#local_actionlib_dir = get_actionlib_dir(local=True, custom=custom)
try:
if clear and Path(dst).exists():
shutil.rmtree(dst)
#set_actionlib_dir(custom=custom)
script = Path(__file__).parent / 'synchronize.py'
cmd = [
sys.executable,
script,
'--src', str(src),
'--dst', str(dst),
'--only-new', json.dumps(only_new),
'--only-recent', json.dumps(only_recent),
]
subprocess.Popen(cmd)
except Exception as e:
print(e)
+465
View File
@@ -0,0 +1,465 @@
# 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.adapter.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.adapter.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.adapter.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)
'''
+42
View File
@@ -0,0 +1,42 @@
import argparse
import fnmatch
import importlib.util
import json
import re
import subprocess
import sys
from pathlib import Path
# import module utils without excuting __init__
spec = importlib.util.spec_from_file_location(
"utils", Path(__file__).parent/"file_utils.py"
)
utils = importlib.util.module_from_spec(spec)
spec.loader.exec_module(utils)
def synchronize(src, dst, only_new=False, only_recent=False):
excludes=['*.sync-conflict-*', '.*']
includes=['*.blend', 'blender_assets.cats.txt']
utils.copy_dir(
src, dst,
only_new=only_new, only_recent=only_recent,
excludes=excludes, includes=includes
)
if __name__ == '__main__' :
parser = argparse.ArgumentParser(description='Add Comment To the tracker',
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument('--src')
parser.add_argument('--dst')
parser.add_argument('--only-new', type=json.loads, default='false')
parser.add_argument('--only-recent', type=json.loads, default='false')
args = parser.parse_args()
synchronize(**vars(args))
+89
View File
@@ -0,0 +1,89 @@
import re
import os
from pathlib import Path
from fnmatch import fnmatch
from glob import glob
class Template:
field_pattern = re.compile(r'{(\w+)\*{0,2}}')
field_pattern_recursive = re.compile(r'{(\w+)\*{2}}')
def __init__(self, template):
#asset_data_path = Path(lib_path) / ASSETLIB_FILENAME
self.template = template
@property
def glob_pattern(self):
pattern = self.field_pattern_recursive.sub('**', self.template)
pattern = self.field_pattern.sub('*', pattern)
return pattern
@property
def re_pattern(self):
pattern = self.field_pattern_recursive.sub('([\\\w -_.\/]+)', self.template)
pattern = self.field_pattern.sub('([\\\w -_.]+)', pattern)
pattern = pattern.replace('?', '.')
pattern = pattern.replace('*', '.*')
return re.compile(pattern)
@property
def fields(self):
return self.field_pattern.findall(self.template)
#return [f or '0' for f in fields]
def parse(self, path):
path = Path(path).as_posix()
res = self.re_pattern.findall(path)
if not res:
print('Could not parse {path} with {self.re_pattern}')
return {}
fields = self.fields
if len(fields) == 1:
field_values = res
else:
field_values = res[0]
return {k:v for k,v in zip(fields, field_values)}
def format(self, data=None, **kargs):
#print('format', self.template, data, kargs)
data = {**(data or {}), **kargs}
try:
path = self.template.format(**data)
except KeyError:
print(f'Cannot format {self.template} with {data}')
return
path = os.path.expandvars(path)
return Path(path)
def glob(self, directory, pattern=None):
'''If pattern is given it need to be absolute'''
if pattern is None:
pattern = Path(directory, self.glob_pattern).as_posix()
for entry in os.scandir(directory):
entry_path = Path(entry.path)
if entry.is_file() and fnmatch(entry_path.as_posix(), pattern):
yield entry_path
elif entry.is_dir():
yield from self.glob(entry.path, pattern)
def find(self, data, **kargs):
pattern = self.format(data, **kargs)
paths = glob(pattern.as_posix())
if paths:
return Path(paths[0])
def __repr__(self):
return f'Template({self.template})'