start refacto update

This commit is contained in:
2024-07-04 11:53:58 +02:00
parent a01b282f45
commit 4465148b22
35 changed files with 1700 additions and 188 deletions
-54
View File
@@ -1,54 +0,0 @@
"""
Util function for this addon
"""
import bpy
from . bl_utils import get_addon_prefs
def thumbnail_blend_file(input_blend, output_img):
input_blend = Path(input_blend).resolve()
output_img = Path(output_img).resolve()
print(f'Thumbnailing {input_blend} to {output_img}')
blender_thumbnailer = Path(bpy.app.binary_path).parent / 'blender-thumbnailer'
output_img.parent.mkdir(exist_ok=True, parents=True)
subprocess.call([blender_thumbnailer, str(input_blend), str(output_img)])
success = output_img.exists()
if not success:
empty_preview = RESOURCES_DIR / 'empty_preview.png'
shutil.copy(str(empty_preview), str(output_img))
return success
def get_active_library():
'''Get the pref library properties from the active library of the asset browser'''
prefs = get_addon_prefs()
lib_ref = bpy.context.space_data.params.asset_library_reference
#Check for merged library
for l in prefs.libraries:
if l.name == lib_ref:
return l
def update_library_path(self, context):
"""Removing all asset libraries and recreate them"""
addon_prefs = get_addon_prefs()
libs = context.preferences.filepaths.asset_libraries
for i, lib in reversed(list(enumerate(libs))):
if (addon_lib := addon_prefs.libraries.get(lib.name)) and addon_lib.path == lib.path:
bpy.ops.preferences.asset_library_remove(index=i)
for addon_lib in addon_prefs.libraries:
if not addon_lib.use:
continue
bpy.ops.preferences.asset_library_add(directory=str(addon_lib.path))
libs[-1].name = addon_lib.name
+97 -4
View File
@@ -2,7 +2,7 @@
"""
Generic Blender functions
"""
import json
from pathlib import Path
from fnmatch import fnmatch
from typing import Any, List, Iterable, Optional, Tuple
@@ -15,6 +15,8 @@ from bpy_extras import asset_utils
from os.path import abspath
import subprocess
from .file_utils import norm_str
class attr_set():
'''Receive a list of tuple [(data_path, "attribute" [, wanted value)] ]
@@ -82,6 +84,9 @@ def get_viewport():
screen = bpy.context.screen
areas = [a for a in screen.areas if a.type == 'VIEW_3D']
if not areas:
return
areas.sort(key=lambda x : x.width*x.height)
return areas[-1]
@@ -216,7 +221,7 @@ def norm_arg(arg_name, format=str.lower, prefix='--', separator='-'):
return prefix + arg_name
def get_bl_cmd(blender=None, background=False, focus=True, blendfile=None, script=None, **kargs):
def get_bl_cmd(blender=None, background=False, factory_startup=False, focus=True, blendfile=None, script=None, **kargs):
cmd = [str(blender)] if blender else [bpy.app.binary_path]
if background:
@@ -228,6 +233,9 @@ def get_bl_cmd(blender=None, background=False, focus=True, blendfile=None, scrip
cmd += ['--python-use-system-env']
if factory_startup:
cmd += ['--factory-startup']
if blendfile:
cmd += [str(blendfile)]
@@ -347,7 +355,10 @@ def split_path(path) :
return bone_name, prop_name
def get_asset_type(asset_type):
data_types = { p.fixed_type.identifier: p.identifier for p in
bpy.types.BlendData.bl_rna.properties if hasattr(p, 'fixed_type')}
return data_types[asset_type]
def load_datablocks(src, names=None, type='objects', link=True, expr=None, assets_only=False) -> list:
@@ -482,4 +493,86 @@ def get_object_libraries(ob):
filepaths.append(absolute_filepath)
return filepaths
return filepaths
def clean_name(name):
if re.match(r'(.*)\.\d{3}$', name):
return name[:-4]
return name
def is_node_groups_duplicate(node_groups):
node_group_types = sorted([n.type for n in node_groups[0].nodes])
return all( sorted([n.type for n in ng.nodes]) ==
node_group_types for ng in node_groups[1:])
def is_images_duplicate(images):
return all( img.filepath == images[0].filepath for image in images)
def is_materials_duplicate(materials):
node_group_types = sorted([n.type for n in materials[0].node_tree.nodes])
return all( sorted([n.type for n in mat.node_tree.nodes]) ==
node_group_types for mat in materials[1:])
def merge_datablock_duplicates(datablocks, blend_data, force=False):
"""Merging materials, node_groups or images based on name .001, .002"""
failed = []
merged = []
datablocks = list(datablocks)
#blend_data = get_asset_type(datablocks[0].bl_rna.identifier)
if blend_data == 'materials':
is_datablock_duplicate = is_materials_duplicate
elif blend_data == 'node_groups':
is_datablock_duplicate = is_node_groups_duplicate
elif blend_data == 'images':
is_datablock_duplicate = is_images_duplicate
else:
raise Exception(f'Type, {blend_data} not supported')
# Group by name
groups = {}
for datablock in images:
groups.setdefault(clean_name(datablock.name), []).append(datablock)
for datablock in blend_data:
name = clean_name(datablock.name)
if name in groups and datablock not in groups[name]:
groups[name].append(datablock)
print("\nMerge Duplicate Datablocks...")
for group in groups.values():
if len(group) == 1:
continue
if not force:
datablocks.sort(key=lambda x : x.name, reverse=True)
for datablock in datablocks[1:]:
is_duplicate = is_datablock_duplicate((datablock, datablocks[0]))
if not is_duplicate and not force:
failed.append((datablock.name, datablocks[0].name))
print(f'Cannot merge Datablock {datablocks.name} with {datablocks[0].name} they are different')
continue
merged.append((datablock.name, datablocks[0].name))
print(f'Merge Datablock {datablock.name} into {datablocks[0].name}')
datablock.user_remap(datablocks[0])
datablocks.remove(datablock)
blend_data.remove(datablock)
# Rename groups if it has no duplicate left
for datablocks in groups.values():
if len(datablocks) == 1 and not datablocks[0].library:
datablocks[0].name = clean_name(datablocks[0].name)
return merged, failed
+8
View File
@@ -2,6 +2,14 @@
from pathlib import Path
import uuid
import bpy
from .file_utils import cache
@cache(1)
def read_catalog(library_path):
catalog = Catalog(library_path)
catalog.read()
return catalog
class CatalogItem:
+40 -1
View File
@@ -12,9 +12,12 @@ from pathlib import Path
import importlib
import sys
import shutil
from functools import wraps
from time import perf_counter
import contextlib
@contextlib.contextmanager
def cd(path):
"""Changes working directory and returns to previous on exit."""
@@ -25,6 +28,7 @@ def cd(path):
finally:
os.chdir(prev_cwd)
def install_module(module_name, package_name=None):
'''Install a python module with pip or return it if already installed'''
try:
@@ -39,6 +43,7 @@ def install_module(module_name, package_name=None):
return module
def import_module_from_path(path):
from importlib import util
@@ -54,6 +59,7 @@ def import_module_from_path(path):
print(f'Cannot import file {path}')
print(e)
def norm_str(string, separator='_', format=str.lower, padding=0):
string = str(string)
string = string.replace('_', ' ')
@@ -73,6 +79,7 @@ def norm_str(string, separator='_', format=str.lower, padding=0):
return string
def remove_version(filepath):
pattern = '_v[0-9]+\.'
search = re.search(pattern, filepath)
@@ -82,12 +89,14 @@ def remove_version(filepath):
return Path(filepath).name
def is_exclude(name, patterns) -> bool:
# from fnmatch import fnmatch
if not isinstance(patterns, (list,tuple)) :
patterns = [patterns]
return any([fnmatch(name, p) for p in patterns])
def get_last_files(root, pattern=r'_v\d{3}\.\w+', only_matching=False, ex_file=None, ex_dir=None, keep=1, verbose=False) -> list:
'''Recursively get last(s) file(s) (when there is multiple versions) in passed directory
root -> str: Filepath of the folder to scan.
@@ -139,6 +148,7 @@ def get_last_files(root, pattern=r'_v\d{3}\.\w+', only_matching=False, ex_file=N
return sorted(files)
def copy_file(src, dst, only_new=False, only_recent=False):
if dst.exists():
if only_new:
@@ -153,6 +163,7 @@ def copy_file(src, dst, only_new=False, only_recent=False):
else:
subprocess.call(['cp', str(src), str(dst)])
def copy_dir(src, dst, only_new=False, only_recent=False, excludes=['.*'], includes=[]):
src, dst = Path(src), Path(dst)
@@ -203,6 +214,7 @@ def open_file(filepath, select=False):
cmd += [str(filepath)]
subprocess.Popen(cmd)
def open_blender_file(filepath=None):
filepath = filepath or bpy.data.filepath
@@ -217,6 +229,32 @@ def open_blender_file(filepath=None):
subprocess.Popen(cmd)
def cache(timeout):
_cache = {}
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
current_time = perf_counter()
cache_key = (*args, *kwargs.items()) # Assuming the first argument is the file path
# Check if the cache is valid
if cache_key in _cache:
cached_content, cache_time = _cache[cache_key]
if (current_time - cache_time) < timeout:
return cached_content
# Execute the function and update the cache
result = func(*args, **kwargs)
_cache[cache_key] = (result, current_time)
return result
return wrapper
return decorator
def read_file(path):
'''Read a file with an extension in (json, yaml, yml, txt)'''
@@ -255,6 +293,7 @@ def read_file(path):
return data
def write_file(path, data, indent=4):
'''Read a file with an extension in (json, yaml, yml, text)'''
@@ -315,4 +354,4 @@ def synchronize(src, dst, only_new=False, only_recent=False, clear=False):
except Exception as e:
print(e)
+319
View File
@@ -0,0 +1,319 @@
"""
Util function for this addon
"""
import os
from pathlib import Path
import inspect
from datetime import datetime
from time import perf_counter
import platform
import bpy
from .catalog import Catalog, read_catalog
from .bl_utils import get_addon_prefs, get_asset_type
from .file_utils import read_file, write_file, cache, import_module_from_path
def thumbnail_blend_file(input_blend, output_img):
input_blend = Path(input_blend).resolve()
output_img = Path(output_img).resolve()
print(f'Thumbnailing {input_blend} to {output_img}')
blender_thumbnailer = Path(bpy.app.binary_path).parent / 'blender-thumbnailer'
output_img.parent.mkdir(exist_ok=True, parents=True)
subprocess.call([blender_thumbnailer, str(input_blend), str(output_img)])
success = output_img.exists()
if not success:
empty_preview = RESOURCES_DIR / 'empty_preview.png'
shutil.copy(str(empty_preview), str(output_img))
return success
def get_active_library():
'''Get the pref library properties from the active library of the asset browser'''
prefs = get_addon_prefs()
lib_ref = bpy.context.space_data.params.asset_library_reference
#Check for merged library
for l in prefs.libraries:
if l.name == lib_ref:
return l
def update_library_path():
"""Removing all asset libraries and recreate them"""
print("update_library_path")
addon_prefs = get_addon_prefs()
libs = bpy.context.preferences.filepaths.asset_libraries
for i, lib in reversed(list(enumerate(libs))):
if (addon_lib := addon_prefs.libraries.get(lib.name)): # and addon_lib.path == lib.path
bpy.ops.preferences.asset_library_remove(index=i)
for addon_lib in addon_prefs.libraries:
if not addon_lib.use:
continue
bpy.ops.preferences.asset_library_add(directory=str(addon_lib.path))
libs[-1].name = addon_lib.name
def load_library_config(config_path):
""""Load library prefs from config path"""
if not config_path:
return []
config_path = Path(config_path)
if not config_path.exists():
print(f'Config {config_path} not exist')
return []
prefs = get_addon_prefs()
libs = []
for lib_dict in read_file(config_path):
lib = prefs.libraries.add()
lib.is_user = False
lib.set_dict(lib_dict)
libs.append(lib)
return libs
def load_libraries():
""""Load library prefs from config pref and env"""
prefs = get_addon_prefs()
bl_libs = bpy.context.preferences.filepaths.asset_libraries
for i, bl_lib in reversed(list(enumerate(bl_libs))):
addon_lib = prefs.libraries.get(bl_lib.name)
if not addon_lib or addon_lib.is_user:
continue
bpy.ops.preferences.asset_library_remove(index=i)
# Remove lib from addons preferences
for i, addon_lib in reversed(list(enumerate(prefs.libraries))):
if not addon_lib.is_user:
prefs.libraries.remove(i)
env_config = os.getenv('ASSET_LIBRARY_CONFIG')
libs = load_library_config(env_config) + load_library_config(prefs.config_path)
return libs
def clear_time_tag(asset):
# Created time tag
for tag in list(asset.asset_data.tags):
try:
datetime.strptime(tag.name, "%Y-%m-%d %H:%M")
asset.asset_data.tags.remove(tag)
except ValueError:
continue
def create_time_tag(asset):
asset.asset_data.tags.new(datetime.now().strftime("%Y-%m-%d %H:%M"))
def version_file(path, save_versions=3):
if path.exists():
for i in range(save_versions):
version = save_versions - i
version_path = path.with_suffix(f'.blend{version}')
if not version_path.exists():
continue
if i == 0:
version_path.unlink()
else:
version_path.rename(path.with_suffix(f'.blend{version+1}'))
path.rename(path.with_suffix(f'.blend1'))
def list_datablocks(blend_file, asset_types={"objects", "materials", "node_groups"}):
blend_data = {}
with bpy.data.temp_data(filepath=str(blend_file)) as temp_data:
with temp_data.libraries.load(str(blend_file), link=True) as (data_from, data_to):
for asset_type in asset_types:
blend_data[asset_type] = getattr(data_from, asset_type)
return blend_data
def get_asset_data(blend_file, asset_type, name, preview=False):
with bpy.data.temp_data(filepath=str(blend_file)) as temp_data:
with temp_data.libraries.load(str(blend_file), link=True) as (data_from, data_to):
if name not in getattr(data_from, asset_type):
return
setattr(data_to, asset_type, [name])
if assets := getattr(data_to, asset_type):
asset = assets[0]
asset_data = asset.asset_data
data = {
"description": asset_data.description,
"catalog_id": asset_data.catalog_id,
"catalog_simple_name": asset_data.catalog_simple_name,
"path": blend_file,
"tags": list(asset_data.tags.keys())
}
if asset.preview and preview:
image_size = asset.preview.image_size
preview_pixels = [0] * image_size[0] * image_size[1] * 4
asset.preview.image_pixels_float.foreach_get(preview_pixels)
data["preview_pixels"] = preview_pixels
data["preview_size"] = list(image_size)
return data
def find_asset_data(name, asset_type, preview=False):
"""Find info about an asset found in library"""
bl_libs = bpy.context.preferences.filepaths.asset_libraries
# First search for a blend with the same name
for bl_lib in bl_libs:
for blend_file in Path(bl_lib.path).glob(f"**/{name}.blend"):
if asset_data := get_asset_data(blend_file, asset_type, name, preview=preview):
asset_data['library'] = bl_lib
return asset_data
# for bl_lib in bl_libs:
# for blend_file in Path(bl_lib.path).glob("**/*.blend"):
# if asset_data := get_asset_data(blend_file, asset_type, name):
# return bl_lib, asset_data
def get_filepath_library(filepath):
for lib in bpy.context.preferences.filepaths.asset_libraries:
if bpy.path.is_subdir(filepath, lib.path):
return lib
def get_asset_catalog_path(asset, fallback=''):
if asset.local_id:
path = Path(bpy.data.filepath).parent
elif (lib := get_filepath_library(asset.full_library_path)):
if lib:
path = lib.path
else:
return fallback
if not (catalog := read_catalog(path)):
return fallback
if not (catalog_item := catalog.get(id=asset.metadata.catalog_id, fallback=fallback)):
return fallback
return catalog_item.path
def get_asset_full_path(asset):
"""Get a path that represent all informations about an asset path/type/catalog/name"""
asset_path = asset.full_library_path
if asset.local_id:
asset_path = f'{bpy.data.filepath}/{asset_path}'
asset_type, asset_name = Path(asset.full_path).parts[-2:]
asset_type = get_asset_type(asset_type)
return Path(asset_path, asset_type, asset_name).as_posix()
def get_asset_source(datablock):
weak_reference = datablock.library_weak_reference
if isinstance(datablock, bpy.types.Object) and datablock.data:
weak_reference = datablock.data.library_weak_reference
if weak_reference and (source_path := Path(weak_reference.filepath)).exists():
return source_path
asset_libraries = context.preferences.filepaths.asset_libraries
for asset_library in asset_libraries:
library_path = Path(asset_library.path)
if blend_files := list(library_path.glob(f"**/{datablock.name}.blend")):
return
return datablock.library_weak_reference
def get_blender_cache_dir():
if platform.system() == 'Linux':
cache_folder = os.path.expandvars('$HOME/.cache/blender')
elif platform.system() == 'Windows':
cache_folder = os.path.expanduser('%USERPROFILE%/AppData/Local/Blender Foundation/Blender')
elif platform.system() == 'Darwin':
cache_folder = '/Library/Caches/Blender'
return Path(cache_folder)
def find_asset_source(library_map, asset_type, name):
return next( (l for l, blend_data in sorted(library_map.items(), key=lambda x: x[1]['st_mtime'], reverse=True)
if name in blend_data['node_groups']), None)
def asset_library_map():
""""Get a mapping of all datablocks of the blend files from the libraries"""
asset_libraries = bpy.context.preferences.filepaths.asset_libraries
cache_file = get_blender_cache_dir() / 'asset-library.json'
cache = None
if cache_file.exists():
cache = read_file(cache_file)
if cache is None:
cache = {}
file_keys = []
for asset_library in asset_libraries:
library_path = Path(asset_library.path)
for bl_file in library_path.glob("**/*.blend"):
file_keys.append(file_key := bl_file.as_posix())
st_mtime = bl_file.stat().st_mtime
if (bl_cache := cache.get(file_key)) and bl_cache.get('st_mtime') >= st_mtime:
continue
datablocks = list_datablocks(bl_file)
cache[file_key] = dict(st_mtime=st_mtime, **datablocks)
# Remove map when the blend not exists anymore
for file_key in list(cache.keys()):
if file_key not in file_keys:
del cache[file_key]
write_file(cache_file, cache)
return cache
# print(perf_counter() - t0)
# t0 = perf_counter()
# for asset_library in asset_libraries:
# library_path = Path(asset_library.path)
# for blend_file in library_path.glob("**/*.blend"):
# with bpy.data.libraries.load(str(blend_file), link=True) as (data_from, data_to):
# node_groups = data_from.node_groups
# print(node_groups)
# print(perf_counter() - t0)