2 Commits
Author SHA1 Message Date
christophe.seux 4465148b22 start refacto update 2024-07-04 11:53:58 +02:00
christophe.seux a01b282f45 start refacto 2024-05-27 17:22:45 +02:00
80 changed files with 3898 additions and 5640 deletions
-2
View File
@@ -1,2 +0,0 @@
# Migrate code style to Black
3d65bb6e4db5d1a81e48a724c6059be317ac35a9
+34 -53
View File
@@ -8,81 +8,62 @@ 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 import sys
from . import operators, properties, ui, preferences, data_type
from .core.lib_utils import load_libraries, update_library_path
from asset_library import pose bl_modules = (
from asset_library import action operators,
from asset_library import collection properties,
from asset_library import file ui,
from asset_library import gui, keymaps, preferences, operators preferences,
from asset_library import constants data_type
)
# 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
# Reload Modules from inside Blender
if "bpy" in locals(): if "bpy" in locals():
print("Reload Addon Asset Library")
import importlib import importlib
importlib.reload(constants) for mod in modules:
importlib.reload(gui) importlib.reload(mod)
importlib.reload(keymaps)
importlib.reload(preferences)
importlib.reload(operators)
importlib.reload(constants)
importlib.reload(action)
importlib.reload(file)
importlib.reload(collection)
import bpy import bpy
import os
# addon_keymaps: List[Tuple[bpy.types.KeyMap, bpy.types.KeyMapItem]] = []
bl_modules = (operators, pose, action, collection, file, keymaps, gui, preferences)
def load_handler(): def load_handler():
print("load_handler") print('load_handler')
load_libraries()
update_library_path()
#set_env_libraries()
#bpy.ops.assetlib.set_paths(all=True)
set_env_libraries() #if not bpy.app.background:
bpy.ops.assetlib.set_paths(all=True) # bpy.ops.assetlib.bundle(blocking=False, mode='AUTO_BUNDLE')
if not bpy.app.background:
bpy.ops.assetlib.bundle(blocking=False, mode="AUTO_BUNDLE")
def register() -> None:
for m in bl_modules: def register():
m.register() """Register the addon Asset Library for Blender"""
for mod in bl_modules:
mod.register()
# prefs = get_addon_prefs()
bpy.app.timers.register(load_handler, first_interval=1) bpy.app.timers.register(load_handler, first_interval=1)
def unregister() -> None: def unregister():
prefs = get_addon_prefs() """Unregister the addon Asset Library for Blender"""
bpy.utils.previews.remove(prefs.previews)
for m in reversed(bl_modules): for mod in reversed(bl_modules):
m.unregister() mod.unregister()
-34
View File
@@ -1,34 +0,0 @@
from asset_library.action import (
gui,
keymaps,
clear_asset,
concat_preview,
operators,
properties,
rename_pose,
# render_preview
)
if "bpy" in locals():
import importlib
importlib.reload(gui)
importlib.reload(keymaps)
importlib.reload(clear_asset)
importlib.reload(concat_preview)
importlib.reload(operators)
importlib.reload(properties)
importlib.reload(rename_pose)
# importlib.reload(render_preview)
import bpy
def register():
operators.register()
keymaps.register()
def unregister():
operators.unregister()
keymaps.unregister()
-49
View File
@@ -1,49 +0,0 @@
import argparse
import bpy
import json
import sys
from pathlib import Path
# sys.path.append(str(Path(__file__).parents[3]))
from asset_library.common.bl_utils import (
get_preview,
)
def clear_asset(action_name="", use_fake_user=False):
scn = bpy.context.scene
action = bpy.data.actions.get(action_name)
if not action:
print(f"No {action_name} not found.")
bpy.ops.wm.quit_blender()
action.asset_clear()
if use_fake_user:
action.use_fake_user = True
else:
preview = get_preview(asset_path=bpy.data.filepath, asset_name=action_name)
if preview:
preview.unlink()
bpy.data.actions.remove(action)
bpy.ops.wm.save_mainfile(filepath=bpy.data.filepath, compress=True, exit=True)
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Add Comment To the tracker",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
parser.add_argument("--action-name")
parser.add_argument("--use-fake-user", type=json.loads, default="false")
if "--" in sys.argv:
index = sys.argv.index("--")
sys.argv = [sys.argv[index - 1], *sys.argv[index + 1 :]]
args = parser.parse_args()
clear_asset(**vars(args))
-63
View File
@@ -1,63 +0,0 @@
import bpy
def draw_context_menu(layout):
params = bpy.context.space_data.params
asset = bpy.context.asset_file_handle
layout.operator(
"assetlib.open_blend", text="Open blend file"
) # .asset = asset.name
layout.operator("assetlib.play_preview", text="Play Preview")
layout.separator()
layout.operator_context = "INVOKE_DEFAULT"
# layout.operator("assetlib.rename_asset", text="Rename Action")
layout.operator("assetlib.remove_assets", text="Remove Assets")
layout.operator("assetlib.edit_data", text="Edit Asset data")
# layout.operator("actionlib.clear_asset", text="Clear Asset (Fake User)").use_fake_user = True
layout.separator()
layout.operator("actionlib.apply_selected_action", text="Apply Pose").flipped = (
False
)
layout.operator(
"actionlib.apply_selected_action", text="Apply Pose (Flipped)"
).flipped = True
layout.separator()
layout.operator(
"poselib.blend_pose_asset_for_keymap", text="Blend Pose"
).flipped = False
layout.operator(
"poselib.blend_pose_asset_for_keymap", text="Blend Pose (Flipped)"
).flipped = True
layout.separator()
layout.operator(
"poselib.pose_asset_select_bones", text="Select Bones"
).selected_side = "CURRENT"
layout.operator(
"poselib.pose_asset_select_bones", text="Select Bones (Flipped)"
).selected_side = "FLIPPED"
layout.operator(
"poselib.pose_asset_select_bones", text="Select Bones (Both)"
).selected_side = "BOTH"
layout.separator()
# layout.operator("asset.library_refresh")
if params.display_type == "THUMBNAIL":
layout.prop_menu_enum(params, "display_size")
def draw_header(layout):
"""Draw the header of the Asset Browser Window"""
layout.separator()
layout.operator("actionlib.store_anim_pose", text="Add Action", icon="FILE_NEW")
-45
View File
@@ -1,45 +0,0 @@
import argparse
import bpy
import json
import re
import sys
from pathlib import Path
# sys.path.append(str(Path(__file__).parents[3]))
from asset_library.common.bl_utils import (
get_preview,
)
def rename_pose(src_name="", dst_name=""):
scn = bpy.context.scene
action = bpy.data.actions.get(src_name)
if not action:
print(f"No {src_name} not found.")
bpy.ops.wm.quit_blender()
action.name = dst_name
preview = get_preview(asset_path=bpy.data.filepath, asset_name=src_name)
if preview:
preview.rename(re.sub(src_name, dst_name, str(preview)))
bpy.ops.wm.save_mainfile(filepath=bpy.data.filepath, compress=True, exit=True)
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Add Comment To the tracker",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
parser.add_argument("--src-name")
parser.add_argument("--dst-name")
if "--" in sys.argv:
index = sys.argv.index("--")
sys.argv = [sys.argv[index - 1], *sys.argv[index + 1 :]]
args = parser.parse_args()
rename_pose(**vars(args))
-15
View File
@@ -1,15 +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"
}
-16
View File
@@ -1,16 +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
-462
View File
@@ -1,462 +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)
"""
-394
View File
@@ -1,394 +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})"
-46
View File
@@ -1,46 +0,0 @@
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))
+22 -12
View File
@@ -3,25 +3,35 @@ import bpy
DATA_TYPE_ITEMS = [ DATA_TYPE_ITEMS = [
("ACTION", "Action", "", "ACTION", 0), ("NodeTree", "Node Group", "", "NODETREE", 0),
("COLLECTION", "Collection", "", "OUTLINER_OB_GROUP_INSTANCE", 1), ("Material", "Material", "", "MATERIAL", 1),
("FILE", "File", "", "FILE", 2), ("Object", "Object", "", "OBJECT_DATA", 2),
("Action", "Action", "", "ACTION", 3),
("Collection", "Collection", "", "OUTLINER_OB_GROUP_INSTANCE", 4),
("File", "File", "", "FILE", 5)
] ]
DATA_TYPE_GEO_ITEMS = [DATA_TYPE_ITEMS[0], DATA_TYPE_ITEMS[2]]
DATA_TYPE_SHADING_ITEMS = [DATA_TYPE_ITEMS[0], DATA_TYPE_ITEMS[1]]
CATALOG_ITEMS = {}
DATA_TYPES = [i[0] for i in DATA_TYPE_ITEMS] DATA_TYPES = [i[0] for i in DATA_TYPE_ITEMS]
ICONS = { ICONS = {identifier: icon for identifier, name, description, icon, number in DATA_TYPE_ITEMS}
identifier: icon for identifier, name, description, icon, number in DATA_TYPE_ITEMS
}
ASSETLIB_FILENAME = "blender_assets.libs.json" 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 = {}
PLUGINS_ITEMS = [('NONE', 'None', '', 0)]
ADAPTER_DIR = MODULE_DIR / "adapters" LIB_DIR = MODULE_DIR / 'libs'
ADAPTERS = [] LIB_ITEMS = []
PREVIEW_ASSETS_SCRIPT = MODULE_DIR / "common" / "preview_assets.py" SCRIPTS_DIR = MODULE_DIR / 'scripts'
PREVIEW_ASSETS_SCRIPT = MODULE_DIR / 'common' / 'preview_assets.py'
#ADD_ASSET_DICT = {} #ADD_ASSET_DICT = {}
+156 -105
View File
@@ -1,27 +1,28 @@
""" """
Generic Blender functions Generic Blender functions
""" """
import json
from pathlib import Path from pathlib import Path
from fnmatch import fnmatch from fnmatch import fnmatch
from typing import Any, List, Iterable, Optional, Tuple from typing import Any, List, Iterable, Optional, Tuple
Datablock = Any 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
from .file_utils import norm_str
class attr_set:
"""Receive a list of tuple [(data_path, "attribute" [, wanted value)] ] class attr_set():
'''Receive a list of tuple [(data_path, "attribute" [, wanted value)] ]
entering with-statement : Store existing values, assign wanted value (if any) entering with-statement : Store existing values, assign wanted value (if any)
exiting with-statement: Restore values to their old values exiting with-statement: Restore values to their old values
""" '''
def __init__(self, attrib_list): def __init__(self, attrib_list):
self.store = [] self.store = []
@@ -37,7 +38,7 @@ class attr_set:
try: try:
setattr(prop, attr, item[2]) setattr(prop, attr, item[2])
except TypeError: except TypeError:
print(f"Cannot set attribute {attr} to {prop}") print(f'Cannot set attribute {attr} to {prop}')
def __enter__(self): def __enter__(self):
return self return self
@@ -50,36 +51,42 @@ class attr_set:
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
cols = [c for c in bpy.data.collections if scn.user_of_id(c)] cols = [c for c in bpy.data.collections if scn.user_of_id(c)]
return next( 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)
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(): 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( view_3d = next((a for a in view_3ds if a.spaces.active.region_3d.view_perspective == 'PERSP'), view_3ds[0])
(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
areas = [a for a in screen.areas if a.type == "VIEW_3D"] areas = [a for a in screen.areas if a.type == 'VIEW_3D']
if not areas:
return
areas.sort(key=lambda x : x.width*x.height) areas.sort(key=lambda x : x.width*x.height)
return areas[-1] return areas[-1]
@@ -95,7 +102,7 @@ def biggest_asset_browser_area(screen: bpy.types.Screen) -> Optional[bpy.types.A
def area_sorting_key(area: bpy.types.Area) -> Tuple[bool, int]: def area_sorting_key(area: bpy.types.Area) -> Tuple[bool, int]:
"""Return area size in pixels.""" """Return area size in pixels."""
return area.width * area.height return (area.width * area.height)
areas = list(suitable_areas(screen)) areas = list(suitable_areas(screen))
if not areas: if not areas:
@@ -154,9 +161,7 @@ def active_catalog_id(asset_browser: bpy.types.Area) -> str:
return params(asset_browser).catalog_id return params(asset_browser).catalog_id
def get_asset_space_params( def get_asset_space_params(asset_browser: bpy.types.Area) -> bpy.types.FileAssetSelectParams:
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)
@@ -165,7 +170,7 @@ def get_asset_space_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:
@@ -174,7 +179,6 @@ def tag_redraw(screen: bpy.types.Screen) -> None:
for area in suitable_areas(screen): for area in suitable_areas(screen):
area.tag_redraw() area.tag_redraw()
# def get_blender_command(file=None, script=None, background=True, **args): # def get_blender_command(file=None, script=None, background=True, **args):
# '''Return a Blender Command as a list to be used in a subprocess''' # '''Return a Blender Command as a list to be used in a subprocess'''
@@ -193,7 +197,6 @@ def tag_redraw(screen: bpy.types.Screen) -> None:
# return cmd # return cmd
def norm_value(value): def norm_value(value):
if isinstance(value, (tuple, list)): if isinstance(value, (tuple, list)):
values = [] values = []
@@ -212,34 +215,35 @@ def norm_value(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( def get_bl_cmd(blender=None, background=False, factory_startup=False, focus=True, blendfile=None, script=None, **kargs):
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]
if background: if background:
cmd += ["--background"] cmd += ['--background']
if not focus and not background: if not focus and not background:
cmd += ["--no-window-focus"] cmd += ['--no-window-focus']
cmd += ["--window-geometry", "5000", "0", "10", "10"] cmd += ['--window-geometry', '5000', '0', '10', '10']
cmd += ["--python-use-system-env"] cmd += ['--python-use-system-env']
if factory_startup:
cmd += ['--factory-startup']
if blendfile: if blendfile:
cmd += [str(blendfile)] cmd += [str(blendfile)]
if script: if script:
cmd += ["--python", str(script)] cmd += ['--python', str(script)]
if kargs: if kargs:
cmd += ["--"] cmd += ['--']
for k, v in kargs.items(): for k, v in kargs.items():
k = norm_arg(k) k = norm_arg(k)
v = norm_value(v) v = norm_value(v)
@@ -254,35 +258,15 @@ def get_bl_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)
else search in master collection else search in master collection
""" '''
if cols is None: if cols is None:
cols = [] cols = []
@@ -299,20 +283,13 @@ def get_col_parents(col, root=None, cols=None):
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
cols = [c for c in bpy.data.collections if scn.user_of_id(c)] cols = [c for c in bpy.data.collections if scn.user_of_id(c)]
return next( 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)
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]: def load_assets_from(filepath: Path) -> List[Datablock]:
@@ -360,8 +337,8 @@ def has_assets(filepath: Path) -> bool:
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)
@@ -373,14 +350,18 @@ def split_path(path):
try : try :
prop_name = path.split('["')[2].split('"]')[0] prop_name = path.split('["')[2].split('"]')[0]
except : except :
prop_name = path.split(".")[-1] prop_name = path.split('.')[-1]
return bone_name, prop_name return bone_name, prop_name
def load_datablocks( def get_asset_type(asset_type):
src, names=None, type="objects", link=True, expr=None, assets_only=False data_types = { p.fixed_type.identifier: p.identifier for p in
) -> list: 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:
return_list = not isinstance(names, str) return_list = not isinstance(names, str)
names = names or [] names = names or []
@@ -391,10 +372,7 @@ def load_datablocks(
pattern = expr pattern = expr
expr = lambda x : fnmatch(x, pattern) expr = lambda x : fnmatch(x, pattern)
with bpy.data.libraries.load(str(src), link=link, assets_only=assets_only) as ( with bpy.data.libraries.load(str(src), link=link,assets_only=assets_only) as (data_from, data_to):
data_from,
data_to,
):
datablocks = getattr(data_from, type) datablocks = getattr(data_from, type)
if expr: if expr:
names += [i for i in datablocks if expr(i)] names += [i for i in datablocks if expr(i)]
@@ -411,26 +389,23 @@ def load_datablocks(
elif datablocks: elif datablocks:
return datablocks[0] return datablocks[0]
""" """
# --- Collection handling # --- Collection handling
""" """
def col_as_asset(col, verbose=False): def col_as_asset(col, verbose=False):
if col is None: if col is None:
return return
if verbose: if verbose:
print("linking:", col.name) print('linking:', col.name)
pcol = bpy.data.collections.new(col.name) pcol = bpy.data.collections.new(col.name)
bpy.context.scene.collection.children.link(pcol) bpy.context.scene.collection.children.link(pcol)
pcol.children.link(col) pcol.children.link(col)
pcol.asset_mark() pcol.asset_mark()
return pcol return pcol
def load_col(filepath, name, link=True, override=True, rig_pattern=None, context=None): 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""" '''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): # 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] # data_to.collections = [c for c in data_from.collections if c == name]
@@ -439,12 +414,12 @@ def load_col(filepath, name, link=True, override=True, rig_pattern=None, context
# return data_to.collections[0] # return data_to.collections[0]
context = context or bpy.context context = context or bpy.context
col = load_datablocks(filepath, name, link=link, type="collections") col = load_datablocks(filepath, name, link=link, type='collections')
## create instance object ## create instance object
inst = bpy.data.objects.new(col.name, None) inst = bpy.data.objects.new(col.name, None)
inst.instance_collection = col inst.instance_collection = col
inst.instance_type = "COLLECTION" inst.instance_type = 'COLLECTION'
context.scene.collection.objects.link(inst) context.scene.collection.objects.link(inst)
# make active # make active
@@ -454,7 +429,7 @@ def load_col(filepath, name, link=True, override=True, rig_pattern=None, context
## simple object (no armatures) ## simple object (no armatures)
if not link or not override: if not link or not override:
return inst return inst
if not next((o for o in col.all_objects if o.type == "ARMATURE"), None): if not next((o for o in col.all_objects if o.type == 'ARMATURE'), None):
return inst return inst
## Create the override ## Create the override
@@ -462,21 +437,18 @@ def load_col(filepath, name, link=True, override=True, rig_pattern=None, context
parent_cols = inst.users_collection parent_cols = inst.users_collection
child_cols = [child for pcol in parent_cols for child in pcol.children] child_cols = [child for pcol in parent_cols for child in pcol.children]
params = {"active_object": inst, "selected_objects": [inst]} params = {'active_object': inst, 'selected_objects': [inst]}
try: try:
bpy.ops.object.make_override_library(params) bpy.ops.object.make_override_library(params)
## check which collection is new in parents collection ## check which collection is new in parents collection
asset_col = next( asset_col = next((c for pcol in parent_cols for c in pcol.children if c not in child_cols), None)
(c for pcol in parent_cols for c in pcol.children if c not in child_cols),
None,
)
if not asset_col: if not asset_col:
print("Overriden, but no collection found !!") print('Overriden, but no collection found !!')
return return
for ob in asset_col.all_objects: for ob in asset_col.all_objects:
if ob.type != "ARMATURE": if ob.type != 'ARMATURE':
continue continue
if rig_pattern and not fnmatch(ob.name, rig_pattern): if rig_pattern and not fnmatch(ob.name, rig_pattern):
continue continue
@@ -488,19 +460,16 @@ def load_col(filepath, name, link=True, override=True, rig_pattern=None, context
return ob return ob
except Exception as e: except Exception as e:
print(f"Override failed on {col.name}") print(f'Override failed on {col.name}')
print(e) print(e)
return inst return inst
def get_preview(asset_path="", asset_name=""): def get_preview(asset_path='', asset_name=''):
asset_preview_dir = Path(asset_path).parents[1] asset_preview_dir = Path(asset_path).parents[1]
name = asset_name.lower() name = asset_name.lower()
return next( return next((f for f in asset_preview_dir.rglob('*') if f.stem.lower().endswith(name)), None)
(f for f in asset_preview_dir.rglob("*") if f.stem.lower().endswith(name)), None
)
def get_object_libraries(ob): def get_object_libraries(ob):
if ob is None: if ob is None:
@@ -510,7 +479,7 @@ def get_object_libraries(ob):
if ob.data: if ob.data:
libraries += [ob.data.library] libraries += [ob.data.library]
if ob.type in ("MESH", "CURVE"): if ob.type in ('MESH', 'CURVE'):
libraries += [m.library for m in ob.data.materials if m] libraries += [m.library for m in ob.data.materials if m]
filepaths = [] filepaths = []
@@ -525,3 +494,85 @@ def get_object_libraries(ob):
filepaths.append(absolute_filepath) filepaths.append(absolute_filepath)
return filepaths return filepaths
def clean_name(name):
if re.match(r'(.*)\.\d{3}$', name):
return name[:-4]
return name
def is_node_groups_duplicate(node_groups):
node_group_types = sorted([n.type for n in node_groups[0].nodes])
return all( sorted([n.type for n in ng.nodes]) ==
node_group_types for ng in node_groups[1:])
def is_images_duplicate(images):
return all( img.filepath == images[0].filepath for image in images)
def is_materials_duplicate(materials):
node_group_types = sorted([n.type for n in materials[0].node_tree.nodes])
return all( sorted([n.type for n in mat.node_tree.nodes]) ==
node_group_types for mat in materials[1:])
def merge_datablock_duplicates(datablocks, blend_data, force=False):
"""Merging materials, node_groups or images based on name .001, .002"""
failed = []
merged = []
datablocks = list(datablocks)
#blend_data = get_asset_type(datablocks[0].bl_rna.identifier)
if blend_data == 'materials':
is_datablock_duplicate = is_materials_duplicate
elif blend_data == 'node_groups':
is_datablock_duplicate = is_node_groups_duplicate
elif blend_data == 'images':
is_datablock_duplicate = is_images_duplicate
else:
raise Exception(f'Type, {blend_data} not supported')
# Group by name
groups = {}
for datablock in images:
groups.setdefault(clean_name(datablock.name), []).append(datablock)
for datablock in blend_data:
name = clean_name(datablock.name)
if name in groups and datablock not in groups[name]:
groups[name].append(datablock)
print("\nMerge Duplicate Datablocks...")
for group in groups.values():
if len(group) == 1:
continue
if not force:
datablocks.sort(key=lambda x : x.name, reverse=True)
for datablock in datablocks[1:]:
is_duplicate = is_datablock_duplicate((datablock, datablocks[0]))
if not is_duplicate and not force:
failed.append((datablock.name, datablocks[0].name))
print(f'Cannot merge Datablock {datablocks.name} with {datablocks[0].name} they are different')
continue
merged.append((datablock.name, datablocks[0].name))
print(f'Merge Datablock {datablock.name} into {datablocks[0].name}')
datablock.user_remap(datablocks[0])
datablocks.remove(datablock)
blend_data.remove(datablock)
# Rename groups if it has no duplicate left
for datablocks in groups.values():
if len(datablocks) == 1 and not datablocks[0].library:
datablocks[0].name = clean_name(datablocks[0].name)
return merged, failed
+27 -26
View File
@@ -1,11 +1,19 @@
from pathlib import Path from pathlib import Path
import uuid import uuid
import bpy import bpy
from .file_utils import cache
@cache(1)
def read_catalog(library_path):
catalog = Catalog(library_path)
catalog.read()
return catalog
class CatalogItem: class CatalogItem:
"""Represent a single item of a catalog""" """Represent a single item of a catalog"""
def __init__(self, catalog, path=None, name=None, id=None): def __init__(self, catalog, path=None, name=None, id=None):
self.catalog = catalog self.catalog = catalog
@@ -25,10 +33,10 @@ class CatalogItem:
def norm_name(self, name): def norm_name(self, name):
"""Get a norm name from a catalog_path entry""" """Get a norm name from a catalog_path entry"""
return name.replace("/", "-") return name.replace('/', '-')
def __repr__(self): def __repr__(self):
return f"CatalogItem(name={self.name}, path={self.path}, id={self.id})" return f'CatalogItem(name={self.name}, path={self.path}, id={self.id})'
class CatalogContext: class CatalogContext:
@@ -60,12 +68,11 @@ class CatalogContext:
if self.active_item: if self.active_item:
return self.active_item.path return self.active_item.path
return "" return ''
class Catalog: class Catalog:
"""Represent the catalog of the blender asset browser library""" """Represent the catalog of the blender asset browser library"""
def __init__(self, directory=None): def __init__(self, directory=None):
self.directory = None self.directory = None
@@ -80,7 +87,7 @@ class Catalog:
def filepath(self): def filepath(self):
"""Get the filepath of the catalog text file relative to the directory""" """Get the filepath of the catalog text file relative to the directory"""
if self.directory: if self.directory:
return self.directory / "blender_assets.cats.txt" return self.directory /'blender_assets.cats.txt'
def read(self): def read(self):
"""Read the catalog file of the library target directory or of the specified directory""" """Read the catalog file of the library target directory or of the specified directory"""
@@ -90,15 +97,13 @@ class Catalog:
self._data.clear() self._data.clear()
print(f"Read catalog from {self.filepath}") print(f'Read catalog from {self.filepath}')
for line in self.filepath.read_text(encoding="utf-8").split("\n"): for line in self.filepath.read_text(encoding="utf-8").split('\n'):
if line.startswith(("VERSION", "#")) or not line: if line.startswith(('VERSION', '#')) or not line:
continue continue
cat_id, cat_path, cat_name = line.split(":") cat_id, cat_path, cat_name = line.split(':')
self._data[cat_id] = CatalogItem( self._data[cat_id] = CatalogItem(self, name=cat_name, id=cat_id, path=cat_path)
self, name=cat_name, id=cat_id, path=cat_path
)
return self return self
@@ -106,9 +111,9 @@ class Catalog:
"""Write the catalog file in the library target directory or of the specified directory""" """Write the catalog file in the library target directory or of the specified directory"""
if not self.filepath: if not self.filepath:
raise Exception(f"Cannot write catalog {self} no filepath setted") raise Exception(f'Cannot write catalog {self} no filepath setted')
lines = ["VERSION 1", ""] lines = ['VERSION 1', '']
catalog_items = list(self) catalog_items = list(self)
if sort: if sort:
@@ -117,8 +122,8 @@ class Catalog:
for catalog_item in catalog_items: for catalog_item in catalog_items:
lines.append(f"{catalog_item.id}:{catalog_item.path}:{catalog_item.name}") lines.append(f"{catalog_item.id}:{catalog_item.path}:{catalog_item.name}")
print(f"Write Catalog at: {self.filepath}") print(f'Write Catalog at: {self.filepath}')
self.filepath.write_text("\n".join(lines), encoding="utf-8") self.filepath.write_text('\n'.join(lines), encoding="utf-8")
def get(self, path=None, id=None, fallback=None): def get(self, path=None, id=None, fallback=None):
"""Found a catalog item by is path or id""" """Found a catalog item by is path or id"""
@@ -143,7 +148,7 @@ class Catalog:
if catalog_item: if catalog_item:
return self._data.pop(catalog_item.id) return self._data.pop(catalog_item.id)
print(f"Warning: {catalog_item} cannot be remove, not in {self}") print(f'Warning: {catalog_item} cannot be remove, not in {self}')
return None return None
def add(self, catalog_path): def add(self, catalog_path):
@@ -166,7 +171,7 @@ class Catalog:
return cat_item return cat_item
def update(self, catalogs): def update(self, catalogs):
"Add or remove catalog entries if on the list given or not" 'Add or remove catalog entries if on the list given or not'
catalogs = set(catalogs) # Remove doubles catalogs = set(catalogs) # Remove doubles
@@ -174,13 +179,9 @@ class Catalog:
removed = [c.path for c in self if c.path not in catalogs] removed = [c.path for c in self if c.path not in catalogs]
if added: if added:
print( print(f'{len(added)} Catalog Entry Added \n{tuple(c.name for c in added[:10])}...\n')
f"{len(added)} Catalog Entry Added \n{tuple(c.name for c in added[:10])}...\n"
)
if removed: if removed:
print( print(f'{len(removed)} Catalog Entry Removed \n{tuple(c.name for c in removed[:10])}...\n')
f"{len(removed)} Catalog Entry Removed \n{tuple(c.name for c in removed[:10])}...\n"
)
for catalog_item in removed: for catalog_item in removed:
self.remove(catalog_item) self.remove(catalog_item)
@@ -204,4 +205,4 @@ class Catalog:
return item in self return item in self
def __repr__(self): def __repr__(self):
return f"Catalog(filepath={self.filepath})" return f'Catalog(filepath={self.filepath})'
+98 -100
View File
@@ -1,3 +1,4 @@
"""Generic python functions to make operation on file and names""" """Generic python functions to make operation on file and names"""
import fnmatch import fnmatch
@@ -11,6 +12,8 @@ from pathlib import Path
import importlib import importlib
import sys import sys
import shutil import shutil
from functools import wraps
from time import perf_counter
import contextlib import contextlib
@@ -27,16 +30,14 @@ def cd(path):
def install_module(module_name, package_name=None): def install_module(module_name, package_name=None):
"""Install a python module with pip or return it if already installed""" '''Install a python module with pip or return it if already installed'''
try: try:
module = importlib.import_module(module_name) module = importlib.import_module(module_name)
except ModuleNotFoundError: except ModuleNotFoundError:
print(f"Installing Module {module_name} ....") print(f'Installing Module {module_name} ....')
subprocess.call([sys.executable, "-m", "ensurepip"]) subprocess.call([sys.executable, '-m', 'ensurepip'])
subprocess.call( subprocess.call([sys.executable, '-m', 'pip', 'install', package_name or module_name])
[sys.executable, "-m", "pip", "install", package_name or module_name]
)
module = importlib.import_module(module_name) module = importlib.import_module(module_name)
@@ -55,38 +56,36 @@ def import_module_from_path(path):
return mod return mod
except Exception as e: except Exception as e:
print(f"Cannot import file {path}") print(f'Cannot import file {path}')
print(e) print(e)
def norm_str(string, separator="_", format=str.lower, padding=0): def norm_str(string, separator='_', format=str.lower, padding=0):
string = str(string) string = str(string)
string = string.replace("_", " ") string = string.replace('_', ' ')
string = string.replace("-", " ") string = string.replace('-', ' ')
string = re.sub("[ ]+", " ", string) string = re.sub('[ ]+', ' ', string)
string = re.sub("[ ]+\/[ ]+", "/", string) string = re.sub('[ ]+\/[ ]+', '/', string)
string = string.strip() string = string.strip()
if format: if format:
string = format(string) string = format(string)
# Padd rightest number # Padd rightest number
string = re.sub(r"(\d+)(?!.*\d)", lambda x: x.group(1).zfill(padding), string) string = re.sub(r'(\d+)(?!.*\d)', lambda x : x.group(1).zfill(padding), string)
string = string.replace(" ", separator) string = string.replace(' ', separator)
string = ( string = unicodedata.normalize('NFKD', string).encode('ASCII', 'ignore').decode("utf-8")
unicodedata.normalize("NFKD", string).encode("ASCII", "ignore").decode("utf-8")
)
return string return string
def remove_version(filepath): def remove_version(filepath):
pattern = "_v[0-9]+\." pattern = '_v[0-9]+\.'
search = re.search(pattern, filepath) search = re.search(pattern, filepath)
if search: if search:
filepath = filepath.replace(search.group()[:-1], "") filepath = filepath.replace(search.group()[:-1], '')
return Path(filepath).name return Path(filepath).name
@@ -98,16 +97,8 @@ def is_exclude(name, patterns) -> bool:
return any([fnmatch(name, p) for p in patterns]) return any([fnmatch(name, p) for p in patterns])
def get_last_files( 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:
root, '''Recursively get last(s) file(s) (when there is multiple versions) in passed directory
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. root -> str: Filepath of the folder to scan.
pattern -> str: Regex pattern to group files. pattern -> str: Regex pattern to group files.
only_matching -> bool: Discard files that aren't matched by regex pattern. only_matching -> bool: Discard files that aren't matched by regex pattern.
@@ -115,7 +106,7 @@ def get_last_files(
ex_dir -> list : List of fn_match pattern of directory name to skip. 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). 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. verbose -> bool: Print infos in console.
""" '''
files = [] files = []
if ex_file is None: if ex_file is None:
@@ -129,9 +120,7 @@ def get_last_files(
dirs = [f for f in all_items if f.is_dir()] dirs = [f for f in all_items if f.is_dir()]
for i in range( for i in range(len(allfiles)-1,-1,-1):# fastest way to iterate on index in reverse
len(allfiles) - 1, -1, -1
): # fastest way to iterate on index in reverse
if not re.search(pattern, allfiles[i].name): if not re.search(pattern, allfiles[i].name):
if only_matching: if only_matching:
allfiles.pop(i) allfiles.pop(i)
@@ -139,10 +128,7 @@ def get_last_files(
files.append(allfiles.pop(i).path) files.append(allfiles.pop(i).path)
# separate remaining files in prefix grouped lists # separate remaining files in prefix grouped lists
lilist = [ lilist = [list(v) for k, v in groupby(allfiles, key=lambda x: re.split(pattern, x.name)[0])]
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 # get only item last of each sorted grouplist
for l in lilist: for l in lilist:
@@ -151,22 +137,14 @@ def get_last_files(
files.append(f.path) files.append(f.path)
if verbose and len(l) > 1: if verbose and len(l) > 1:
print( print(f'{root}: keep {str([x.name for x in versions])} out of {len(l)} elements')
f"{root}: keep {str([x.name for x in versions])} out of {len(l)} elements"
)
for d in dirs: # recursively treat all detected directory for d in dirs: # recursively treat all detected directory
if ex_dir and is_exclude(d.name, ex_dir): if ex_dir and is_exclude(d.name, ex_dir):
# skip folder with excluded name # skip folder with excluded name
continue continue
files += get_last_files( files += get_last_files(
d.path, d.path, pattern=pattern, only_matching=only_matching, ex_file=ex_file, ex_dir=ex_dir, keep=keep)
pattern=pattern,
only_matching=only_matching,
ex_file=ex_file,
ex_dir=ex_dir,
keep=keep,
)
return sorted(files) return sorted(files)
@@ -179,20 +157,20 @@ def copy_file(src, dst, only_new=False, only_recent=False):
return return
dst.parent.mkdir(exist_ok=True, parents=True) dst.parent.mkdir(exist_ok=True, parents=True)
print(f"Copy file from {src} to {dst}") print(f'Copy file from {src} to {dst}')
if platform.system() == "Windows": if platform.system() == 'Windows':
subprocess.call(["copy", str(src), str(dst)], shell=True) subprocess.call(['copy', str(src), str(dst)], shell=True)
else: else:
subprocess.call(["cp", str(src), str(dst)]) subprocess.call(['cp', str(src), str(dst)])
def copy_dir(src, dst, only_new=False, only_recent=False, excludes=[".*"], includes=[]): def copy_dir(src, dst, only_new=False, only_recent=False, excludes=['.*'], includes=[]):
src, dst = Path(src), Path(dst) src, dst = Path(src), Path(dst)
if includes: if includes:
includes = r"|".join([fnmatch.translate(x) for x in includes]) includes = r'|'.join([fnmatch.translate(x) for x in includes])
if excludes: if excludes:
excludes = r"|".join([fnmatch.translate(x) for x in excludes]) excludes = r'|'.join([fnmatch.translate(x) for x in excludes])
if dst.is_dir(): if dst.is_dir():
dst.mkdir(exist_ok=True, parents=True) dst.mkdir(exist_ok=True, parents=True)
@@ -203,7 +181,7 @@ def copy_dir(src, dst, only_new=False, only_recent=False, excludes=[".*"], inclu
copy_file(src, dst, only_new=only_new, only_recent=only_recent) copy_file(src, dst, only_new=only_new, only_recent=only_recent)
elif src.is_dir(): elif src.is_dir():
src_files = list(src.rglob("*")) src_files = list(src.rglob('*'))
if excludes: if excludes:
src_files = [f for f in src_files if not re.match(excludes, f.name)] src_files = [f for f in src_files if not re.match(excludes, f.name)]
@@ -216,24 +194,22 @@ def copy_dir(src, dst, only_new=False, only_recent=False, excludes=[".*"], inclu
if src_file.is_dir(): if src_file.is_dir():
dst_file.mkdir(exist_ok=True, parents=True) dst_file.mkdir(exist_ok=True, parents=True)
else: else:
copy_file( copy_file(src_file, dst_file, only_new=only_new, only_recent=only_recent)
src_file, dst_file, only_new=only_new, only_recent=only_recent
)
def open_file(filepath, select=False): def open_file(filepath, select=False):
"""Open a filepath inside the os explorer""" '''Open a filepath inside the os explorer'''
if platform.system() == "Darwin": # macOS if platform.system() == 'Darwin': # macOS
cmd = ["open"] cmd = ['open']
elif platform.system() == "Windows": # Windows elif platform.system() == 'Windows': # Windows
cmd = ["explorer"] cmd = ['explorer']
if select: if select:
cmd += ["/select,"] cmd += ['/select,']
else: # linux variants else: # linux variants
cmd = ["xdg-open"] cmd = ['xdg-open']
if select: if select:
cmd = ["nemo"] cmd = ['nemo']
cmd += [str(filepath)] cmd += [str(filepath)]
subprocess.Popen(cmd) subprocess.Popen(cmd)
@@ -245,8 +221,8 @@ def open_blender_file(filepath=None):
cmd = sys.argv cmd = sys.argv
# if no filepath, use command as is to reopen blender # if no filepath, use command as is to reopen blender
if filepath != "": if filepath != '':
if len(cmd) > 1 and cmd[1].endswith(".blend"): if len(cmd) > 1 and cmd[1].endswith('.blend'):
cmd[1] = str(filepath) cmd[1] = str(filepath)
else: else:
cmd.insert(1, str(filepath)) cmd.insert(1, str(filepath))
@@ -254,38 +230,63 @@ def open_blender_file(filepath=None):
subprocess.Popen(cmd) subprocess.Popen(cmd)
def read_file(path): def cache(timeout):
"""Read a file with an extension in (json, yaml, yml, txt)""" _cache = {}
exts = (".json", ".yaml", ".yml", ".txt") 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)'''
exts = ('.json', '.yaml', '.yml', '.txt')
if not path: if not path:
print("Try to read empty file") print('Try to read empty file')
path = Path(path) path = Path(path)
if not path.exists(): if not path.exists():
print("File not exist", path) print('File not exist', path)
return return
if path.suffix not in exts: if path.suffix not in exts:
print(f"Cannot read file {path}, extension must be in {exts}") print(f'Cannot read file {path}, extension must be in {exts}')
return return
txt = path.read_text() txt = path.read_text()
data = None data = None
if path.suffix.lower() in (".yaml", ".yml"): if path.suffix.lower() in ('.yaml', '.yml'):
yaml = install_module("yaml") yaml = install_module('yaml')
try: try:
data = yaml.safe_load(txt) data = yaml.safe_load(txt)
except Exception: except Exception:
print(f"Could not load yaml file {path}") print(f'Could not load yaml file {path}')
return return
elif path.suffix.lower() == ".json": elif path.suffix.lower() == '.json':
try: try:
data = json.loads(txt) data = json.loads(txt)
except Exception: except Exception:
print(f"Could not load json file {path}") print(f'Could not load json file {path}')
return return
else: else:
data = txt data = txt
@@ -294,37 +295,37 @@ def read_file(path):
def write_file(path, data, indent=4): def write_file(path, data, indent=4):
"""Read a file with an extension in (json, yaml, yml, text)""" '''Read a file with an extension in (json, yaml, yml, text)'''
exts = (".json", ".yaml", ".yml", ".txt") exts = ('.json', '.yaml', '.yml', '.txt')
if not path: if not path:
print("Try to write empty file") print('Try to write empty file')
path = Path(path) path = Path(path)
path.parent.mkdir(parents=True, exist_ok=True) path.parent.mkdir(parents=True, exist_ok=True)
if path.suffix not in exts: if path.suffix not in exts:
print(f"Cannot read file {path}, extension must be in {exts}") print(f'Cannot read file {path}, extension must be in {exts}')
return return
if path.suffix.lower() in (".yaml", ".yml"): if path.suffix.lower() in ('.yaml', '.yml'):
yaml = install_module("yaml") yaml = install_module('yaml')
try: try:
path.write_text(yaml.dump(data), encoding="utf8") path.write_text(yaml.dump(data), encoding='utf8')
except Exception as e: except Exception as e:
print(e) print(e)
print(f"Could not write yaml file {path}") print(f'Could not write yaml file {path}')
return return
elif path.suffix.lower() == ".json": elif path.suffix.lower() == '.json':
try: try:
path.write_text(json.dumps(data, indent=indent), encoding="utf8") path.write_text(json.dumps(data, indent=indent), encoding='utf8')
except Exception as e: except Exception as e:
print(e) print(e)
print(f"Could not write json file {path}") print(f'Could not write json file {path}')
return return
else: else:
data = path.write_text(data, encoding="utf8") data = path.write_text(data, encoding='utf8')
def synchronize(src, dst, only_new=False, only_recent=False, clear=False): def synchronize(src, dst, only_new=False, only_recent=False, clear=False):
@@ -338,22 +339,19 @@ def synchronize(src, dst, only_new=False, only_recent=False, clear=False):
#set_actionlib_dir(custom=custom) #set_actionlib_dir(custom=custom)
script = Path(__file__).parent / "synchronize.py" script = Path(__file__).parent / 'synchronize.py'
cmd = [ cmd = [
sys.executable, sys.executable,
script, script,
"--src", '--src', str(src),
str(src), '--dst', str(dst),
"--dst", '--only-new', json.dumps(only_new),
str(dst), '--only-recent', json.dumps(only_recent),
"--only-new",
json.dumps(only_new),
"--only-recent",
json.dumps(only_recent),
] ]
subprocess.Popen(cmd) subprocess.Popen(cmd)
except Exception as e: except Exception as e:
print(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)
+20 -21
View File
@@ -9,26 +9,25 @@ import string
class TemplateFormatter(string.Formatter): class TemplateFormatter(string.Formatter):
def format_field(self, value, format_spec): def format_field(self, value, format_spec):
if isinstance(value, str): if isinstance(value, str):
spec, sep = [*format_spec.split(":"), None][:2] spec, sep = [*format_spec.split(':'), None][:2]
if sep: if sep:
value = value.replace("_", " ") value = value.replace('_', ' ')
value = value = re.sub(r"([a-z])([A-Z])", rf"\1{sep}\2", value) value = value = re.sub(r'([a-z])([A-Z])', rf'\1{sep}\2', value)
value = value.replace(" ", sep) value = value.replace(' ', sep)
if spec == "u": if spec == 'u':
value = value.upper() value = value.upper()
elif spec == "l": elif spec == 'l':
value = value.lower() value = value.lower()
elif spec == "t": elif spec == 't':
value = value.title() value = value.title()
return super().format(value, format_spec) return super().format(value, format_spec)
class Template: class Template:
field_pattern = re.compile(r"{(\w+)\*{0,2}}") field_pattern = re.compile(r'{(\w+)\*{0,2}}')
field_pattern_recursive = re.compile(r"{(\w+)\*{2}}") field_pattern_recursive = re.compile(r'{(\w+)\*{2}}')
def __init__(self, template): def __init__(self, template):
#asset_data_path = Path(lib_path) / ASSETLIB_FILENAME #asset_data_path = Path(lib_path) / ASSETLIB_FILENAME
@@ -38,16 +37,16 @@ class Template:
@property @property
def glob_pattern(self): def glob_pattern(self):
pattern = self.field_pattern_recursive.sub("**", self.raw) pattern = self.field_pattern_recursive.sub('**', self.raw)
pattern = self.field_pattern.sub("*", pattern) pattern = self.field_pattern.sub('*', pattern)
return pattern return pattern
@property @property
def re_pattern(self): def re_pattern(self):
pattern = self.field_pattern_recursive.sub("([\\\w -_.\/]+)", self.raw) pattern = self.field_pattern_recursive.sub('([\\\w -_.\/]+)', self.raw)
pattern = self.field_pattern.sub("([\\\w -_.]+)", pattern) pattern = self.field_pattern.sub('([\\\w -_.]+)', pattern)
pattern = pattern.replace("?", ".") pattern = pattern.replace('?', '.')
pattern = pattern.replace("*", ".*") pattern = pattern.replace('*', '.*')
return re.compile(pattern) return re.compile(pattern)
@@ -62,7 +61,7 @@ class Template:
res = self.re_pattern.findall(path) res = self.re_pattern.findall(path)
if not res: if not res:
print("Could not parse {path} with {self.re_pattern}") print('Could not parse {path} with {self.re_pattern}')
return {} return {}
fields = self.fields fields = self.fields
@@ -93,14 +92,14 @@ class Template:
#print('FORMAT', self.raw, data) #print('FORMAT', self.raw, data)
path = self.formatter.format(self.raw, **self.norm_data(data)) path = self.formatter.format(self.raw, **self.norm_data(data))
except KeyError as e: except KeyError as e:
print(f"Cannot format {self.raw} with {data}, field {e} is missing") print(f'Cannot format {self.raw} with {data}, field {e} is missing')
return return
path = os.path.expandvars(path) path = os.path.expandvars(path)
return Path(path) return Path(path)
def glob(self, directory, pattern=None): def glob(self, directory, pattern=None):
"""If pattern is given it need to be absolute""" '''If pattern is given it need to be absolute'''
if pattern is None: if pattern is None:
pattern = Path(directory, self.glob_pattern).as_posix() pattern = Path(directory, self.glob_pattern).as_posix()
@@ -115,7 +114,7 @@ class Template:
pattern = self.format(data, **kargs) pattern = self.format(data, **kargs)
pattern_str = str(pattern) pattern_str = str(pattern)
if "*" not in pattern_str and "?" not in pattern_str: if '*' not in pattern_str and '?' not in pattern_str:
return pattern return pattern
paths = glob(pattern.as_posix()) paths = glob(pattern.as_posix())
@@ -125,4 +124,4 @@ class Template:
#return pattern #return pattern
def __repr__(self): def __repr__(self):
return f"Template({self.raw})" return f'Template({self.raw})'
+26
View File
@@ -0,0 +1,26 @@
from . node import ui as node_ui
from . node import operator as node_operator
from . material import ui as material_ui
from . material import operator as material_operator
bl_modules = (
node_ui,
node_operator,
material_ui,
material_operator
)
def register():
"""Register the addon Asset Library for Blender"""
for mod in bl_modules:
mod.register()
def unregister():
"""Unregister the addon Asset Library for Blender"""
for mod in reversed(bl_modules):
mod.unregister()
+16
View File
@@ -0,0 +1,16 @@
from asset_library.data_type.action import (
keymaps,
operators,
)
import bpy
def register():
operators.register()
keymaps.register()
def unregister():
operators.unregister()
keymaps.unregister()
@@ -1,11 +1,12 @@
import sys import sys
from pathlib import Path from pathlib import Path
#sys.path.append(str(Path(__file__).parents[3])) #sys.path.append(str(Path(__file__).parents[3]))
from asset_library.action.concat_preview import mosaic_export from asset_library.data_type.action.concat_preview import mosaic_export
from asset_library.common.file_utils import open_file from asset_library.common.file_utils import open_file
from asset_library.action.functions import reset_bone, get_keyframes from asset_library.data_type.action.functions import reset_bone, get_keyframes
from asset_library.common.functions import read_catalog from asset_library.common.functions import read_catalog
import bpy import bpy
@@ -18,19 +19,17 @@ import subprocess
from tempfile import gettempdir from tempfile import gettempdir
def rm_tree(pth): def rm_tree(pth):
pth = Path(pth) pth = Path(pth)
for child in pth.glob("*"): for child in pth.glob('*'):
if child.is_file(): if child.is_file():
child.unlink() child.unlink()
else: else:
rm_tree(child) rm_tree(child)
pth.rmdir() pth.rmdir()
def render_preview(directory, asset_catalog, render_actions, publish_actions, remove_folder):
def render_preview(
directory, asset_catalog, render_actions, publish_actions, remove_folder
):
scn = bpy.context.scene scn = bpy.context.scene
rnd = bpy.context.scene.render rnd = bpy.context.scene.render
@@ -40,32 +39,27 @@ def render_preview(
blendfile = Path(bpy.data.filepath) blendfile = Path(bpy.data.filepath)
asset_catalog_data = read_catalog(asset_catalog) asset_catalog_data = read_catalog(asset_catalog)
anim_render_dir = ( anim_render_dir = Path(gettempdir()) / 'actionlib_render' #/tmp/actionlib_render. Removed at the end
Path(gettempdir()) / "actionlib_render"
) # /tmp/actionlib_render. Removed at the end
anim_render_dir.mkdir(exist_ok=True, parents=True) anim_render_dir.mkdir(exist_ok=True, parents=True)
preview_render_dir = Path(directory) / "preview" preview_render_dir = Path(directory) / 'preview'
if preview_render_dir.exists() and remove_folder: if preview_render_dir.exists() and remove_folder:
rm_tree(preview_render_dir) rm_tree(preview_render_dir)
preview_render_dir.mkdir(exist_ok=True, parents=True) preview_render_dir.mkdir(exist_ok=True, parents=True)
for i in ("anim", "pose"): for i in ('anim', 'pose'):
Path(preview_render_dir / i).mkdir(exist_ok=True, parents=True) Path(preview_render_dir / i).mkdir(exist_ok=True, parents=True)
for f in preview_render_dir.rglob("*"): for f in preview_render_dir.rglob('*'):
if f.is_dir(): if f.is_dir():
print(f"{f} is dir. Skipped.") print(f'{f} is dir. Skipped.')
continue continue
if ( if all(i not in f.parts for i in ('anim', 'pose')) and f.parent.parts[-1] != 'preview':
all(i not in f.parts for i in ("anim", "pose")) print(f'{f} is out of pipe. Approved or Rtk pictures. Skipped.')
and f.parent.parts[-1] != "preview"
):
print(f"{f} is out of pipe. Approved or Rtk pictures. Skipped.")
continue continue
if not any(f.stem.endswith(a) for a in publish_actions): if not any(f.stem.endswith(a) for a in publish_actions):
print(f"{str(f)} not in publish actions anymore. Removing...") print(f'{str(f)} not in publish actions anymore. Removing...')
f.unlink() f.unlink()
# Set Scene # Set Scene
@@ -76,7 +70,7 @@ def render_preview(
scn.tool_settings.use_keyframe_insert_auto = False scn.tool_settings.use_keyframe_insert_auto = False
# Render Setting # Render Setting
rnd.engine = "BLENDER_EEVEE" rnd.engine = 'BLENDER_EEVEE'
rnd.use_simplify = False rnd.use_simplify = False
rnd.use_stamp_date = True rnd.use_stamp_date = True
rnd.use_stamp_time = True rnd.use_stamp_time = True
@@ -95,7 +89,7 @@ def render_preview(
rnd.use_stamp = True rnd.use_stamp = True
rnd.stamp_font_size = 16 rnd.stamp_font_size = 16
rnd.use_stamp_labels = False rnd.use_stamp_labels = False
rnd.image_settings.file_format = "JPEG" rnd.image_settings.file_format = 'JPEG'
# Viewport Look # Viewport Look
# ---------- # ----------
@@ -114,104 +108,94 @@ def render_preview(
""" """
# Cycles Mat Shading # Cycles Mat Shading
for a in bpy.context.screen.areas: for a in bpy.context.screen.areas:
if a.type == "VIEW_3D": if a.type == 'VIEW_3D':
a.spaces[0].overlay.show_overlays = False a.spaces[0].overlay.show_overlays = False
a.spaces[0].region_3d.view_perspective = "CAMERA" a.spaces[0].region_3d.view_perspective = 'CAMERA'
a.spaces[0].shading.show_cavity = True a.spaces[0].shading.show_cavity = True
a.spaces[0].shading.cavity_type = "WORLD" a.spaces[0].shading.cavity_type = 'WORLD'
a.spaces[0].shading.cavity_ridge_factor = 0.75 a.spaces[0].shading.cavity_ridge_factor = 0.75
a.spaces[0].shading.cavity_valley_factor = 1.0 a.spaces[0].shading.cavity_valley_factor = 1.0
# Add Subsurf # Add Subsurf
# ----------- # -----------
deform_ob = [ deform_ob = [m.object for o in scn.objects \
m.object for o in scn.objects for m in o.modifiers if m.type == "MESH_DEFORM" for m in o.modifiers if m.type == 'MESH_DEFORM'
] ]
deform_ob += [ deform_ob += [m.target for o in scn.objects \
m.target for o in scn.objects for m in o.modifiers if m.type == "SURFACE_DEFORM" for m in o.modifiers if m.type == 'SURFACE_DEFORM'
] ]
objects = [ objects = [o for o in bpy.context.scene.objects if (o.type == 'MESH'
o and o not in deform_ob and o not in bpy.context.scene.collection.objects[:])
for o in bpy.context.scene.objects
if (
o.type == "MESH"
and o not in deform_ob
and o not in bpy.context.scene.collection.objects[:]
)
] ]
for o in objects: for o in objects:
subsurf = False subsurf = False
for m in o.modifiers: for m in o.modifiers:
if m.type == "SUBSURF": if m.type == 'SUBSURF':
m.show_viewport = m.show_render m.show_viewport = m.show_render
m.levels = m.render_levels m.levels = m.render_levels
subsurf = True subsurf = True
break break
if not subsurf: if not subsurf:
subsurf = o.modifiers.new("", "SUBSURF") subsurf = o.modifiers.new('', 'SUBSURF')
subsurf.show_viewport = subsurf.show_render subsurf.show_viewport = subsurf.show_render
subsurf.levels = subsurf.render_levels subsurf.levels = subsurf.render_levels
# Loop through action and render # Loop through action and render
# ------------------------------ # ------------------------------
rig = next((o for o in scn.objects if o.type == "ARMATURE"), None) rig = next((o for o in scn.objects if o.type == 'ARMATURE'), None)
# actions = [a for a in bpy.data.actions if a.asset_data] # actions = [a for a in bpy.data.actions if a.asset_data]
rig.animation_data_create() rig.animation_data_create()
for action_name in render_actions: for action_name in render_actions:
action = bpy.data.actions.get(action_name) action = bpy.data.actions.get(action_name)
if not action: if not action:
print(f"'{action_name}' not found.") print(f'\'{action_name}\' not found.')
continue continue
print(f"-- Current --: {action.name}") print(f"-- Current --: {action.name}")
rnd.stamp_note_text = "{type} : {pose_name}" rnd.stamp_note_text = '{type} : {pose_name}'
action_data = action.asset_data action_data = action.asset_data
if "camera" not in action_data.keys(): if 'camera' not in action_data.keys():
report.append(f"'{action.name}' has no CameraData.") report.append(f"'{action.name}' has no CameraData.")
continue continue
catalog_name = next( catalog_name = next((v['name'] for v in asset_catalog_data.values() if action_data.catalog_id == v['id']), None)
( pose_name = '/'.join([*catalog_name.split('-'), action.name])
v["name"] filename = bpy.path.clean_name(f'{catalog_name}_{action.name}')
for v in asset_catalog_data.values() ext = 'jpg'
if action_data.catalog_id == v["id"]
),
None,
)
pose_name = "/".join([*catalog_name.split("-"), action.name])
filename = bpy.path.clean_name(f"{catalog_name}_{action.name}")
ext = "jpg"
rig.animation_data.action = None rig.animation_data.action = None
bpy.context.view_layer.update() bpy.context.view_layer.update()
for b in rig.pose.bones: for b in rig.pose.bones:
if re.match("^[A-Z]+\.", b.name): if re.match('^[A-Z]+\.', b.name):
continue continue
reset_bone(b) reset_bone(b)
rest_pose = None rest_pose = None
if isinstance(action.asset_data.get("rest_pose"), str): if isinstance(action.asset_data.get('rest_pose'), str):
rest_pose = bpy.data.actions.get(action.asset_data["rest_pose"]) rest_pose = bpy.data.actions.get(action.asset_data['rest_pose'])
rig.animation_data.action = rest_pose rig.animation_data.action = rest_pose
bpy.context.view_layer.update() bpy.context.view_layer.update()
rig.animation_data.action = action rig.animation_data.action = action
if "camera" in action.asset_data.keys(): if 'camera' in action.asset_data.keys():
action_cam = bpy.data.objects.get(action.asset_data["camera"], "") action_cam = bpy.data.objects.get(action.asset_data['camera'], '')
if action_cam: if action_cam:
scn.camera = action_cam scn.camera = action_cam
# Is Anim # Is Anim
if not action_data["is_single_frame"] or "anim" in action_data.tags.keys(): if not action_data['is_single_frame'] or 'anim' in action_data.tags.keys():
keyframes = get_keyframes(action) keyframes = get_keyframes(action)
if not keyframes: if not keyframes:
continue continue
@@ -219,65 +203,57 @@ def render_preview(
anim_end = keyframes[-1] anim_end = keyframes[-1]
if anim_start < scn.frame_start: if anim_start < scn.frame_start:
report.append( report.append(f"Issue found for '{action.name}'. Has keyframes before 'Start Frame'.")
f"Issue found for '{action.name}'. Has keyframes before 'Start Frame'."
)
continue continue
scn.frame_preview_start = anim_start scn.frame_preview_start = anim_start
scn.frame_preview_end = anim_end scn.frame_preview_end = anim_end
rnd.stamp_note_text = rnd.stamp_note_text.format( rnd.stamp_note_text = rnd.stamp_note_text.format(
type="ANIM", type='ANIM',
pose_name=pose_name, pose_name=pose_name,
) )
rnd.filepath = f"{str(anim_render_dir)}/{filename}_####.{ext}" rnd.filepath = f'{str(anim_render_dir)}/{filename}_####.{ext}'
bpy.ops.render.opengl(animation=True) bpy.ops.render.opengl(animation=True)
ffmpeg_cmd = [ ffmpeg_cmd = [
"ffmpeg", 'ffmpeg', '-y',
"-y", '-start_number', f'{anim_start:04d}',
"-start_number", '-i', rnd.filepath.replace('####', '%04d'),
f"{anim_start:04d}", '-c:v', 'libx264',
"-i", str((preview_render_dir/'anim'/filename).with_suffix('.mov')),
rnd.filepath.replace("####", "%04d"),
"-c:v",
"libx264",
str((preview_render_dir / "anim" / filename).with_suffix(".mov")),
] ]
subprocess.call(ffmpeg_cmd) subprocess.call(ffmpeg_cmd)
# Is Pose # Is Pose
elif action_data["is_single_frame"] or "pose" in action_data.tags.keys(): elif action_data['is_single_frame'] or 'pose' in action_data.tags.keys():
scn.frame_preview_start = scn.frame_preview_end = scn.frame_start scn.frame_preview_start = scn.frame_preview_end = scn.frame_start
rnd.stamp_note_text = rnd.stamp_note_text.format( rnd.stamp_note_text = rnd.stamp_note_text.format(
type="POSE", type='POSE',
pose_name=pose_name, pose_name=pose_name,
) )
rnd.filepath = f"{str(preview_render_dir)}/pose/{filename}_####.{ext}" rnd.filepath = f'{str(preview_render_dir)}/pose/{filename}_####.{ext}'
bpy.ops.render.opengl(animation=True) bpy.ops.render.opengl(animation=True)
filename = rnd.filepath.replace("####", f"{scn.frame_preview_end:04d}") filename = rnd.filepath.replace('####', f'{scn.frame_preview_end:04d}')
Path(filename).rename(re.sub("_[0-9]{4}.", ".", filename)) Path(filename).rename(re.sub('_[0-9]{4}.', '.', filename))
shutil.rmtree(anim_render_dir) shutil.rmtree(anim_render_dir)
# Report # Report
# ------ # ------
if report: if report:
report_file = blendfile.parent / Path(f"{blendfile.stem}report").with_suffix( report_file = blendfile.parent / Path(f'{blendfile.stem}report').with_suffix('.txt')
".txt"
)
if not report_file.exists(): if not report_file.exists():
report_file.touch(exist_ok=False) report_file.touch(exist_ok=False)
report_file.write_text("-") report_file.write_text('-')
report_file.write_text("\n".join(report)) report_file.write_text('\n'.join(report))
result = report_file result = report_file
@@ -286,39 +262,30 @@ def render_preview(
open_file(result) open_file(result)
files = [str(f) for f in sorted((preview_render_dir / "pose").glob("*.jpg"))] files = [str(f) for f in sorted((preview_render_dir/'pose').glob('*.jpg'))]
mosaic_export( mosaic_export(
files=files, files=files, catalog_data=asset_catalog_data,
catalog_data=asset_catalog_data, row=2, columns=2, auto_calculate=True,
row=2, bg_color=(0.18, 0.18, 0.18,), resize_output=100
columns=2,
auto_calculate=True,
bg_color=(
0.18,
0.18,
0.18,
),
resize_output=100,
) )
bpy.ops.wm.quit_blender() bpy.ops.wm.quit_blender()
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Add Comment To the tracker",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
parser.add_argument("--directory") if __name__ == '__main__' :
parser.add_argument("--asset-catalog") parser = argparse.ArgumentParser(description='Add Comment To the tracker',
parser.add_argument("--render-actions", nargs="+") formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument("--publish-actions", nargs="+")
parser.add_argument("--remove-folder", type=json.loads, default="false")
if "--" in sys.argv: parser.add_argument('--directory')
index = sys.argv.index("--") parser.add_argument('--asset-catalog')
parser.add_argument('--render-actions', nargs='+')
parser.add_argument('--publish-actions', nargs='+')
parser.add_argument('--remove-folder', type=json.loads, default='false')
if '--' in sys.argv :
index = sys.argv.index('--')
sys.argv = [sys.argv[index-1], *sys.argv[index+1:]] sys.argv = [sys.argv[index-1], *sys.argv[index+1:]]
args = parser.parse_args() args = parser.parse_args()
+48
View File
@@ -0,0 +1,48 @@
import argparse
import bpy
import json
import sys
from pathlib import Path
#sys.path.append(str(Path(__file__).parents[3]))
from asset_library.common.bl_utils import (
get_preview,
)
def clear_asset(action_name='', use_fake_user=False):
scn = bpy.context.scene
action = bpy.data.actions.get(action_name)
if not action:
print(f'No {action_name} not found.')
bpy.ops.wm.quit_blender()
action.asset_clear()
if use_fake_user:
action.use_fake_user = True
else:
preview = get_preview(asset_path=bpy.data.filepath, asset_name=action_name)
if preview:
preview.unlink()
bpy.data.actions.remove(action)
bpy.ops.wm.save_mainfile(
filepath=bpy.data.filepath, compress=True, exit=True
)
if __name__ == '__main__' :
parser = argparse.ArgumentParser(description='Add Comment To the tracker',
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument('--action-name')
parser.add_argument('--use-fake-user', type=json.loads, default='false')
if '--' in sys.argv :
index = sys.argv.index('--')
sys.argv = [sys.argv[index-1], *sys.argv[index+1:]]
args = parser.parse_args()
clear_asset(**vars(args))
@@ -1,3 +1,4 @@
import bpy import bpy
import math import math
import numpy as np import numpy as np
@@ -18,11 +19,9 @@ def alpha_to_color(pixels_data, color):
# print(new_pixels_data)#Dbg # print(new_pixels_data)#Dbg
return new_pixels_data return new_pixels_data
def create_array(height, width): def create_array(height, width):
return np.zeros((height*width*4), dtype=np.float32) return np.zeros((height*width*4), dtype=np.float32)
def read_pixels_data(img, source_height, source_width): def read_pixels_data(img, source_height, source_width):
img_w, img_h = img.size img_w, img_h = img.size
@@ -44,7 +43,6 @@ def read_pixels_data(img, source_height, source_width):
return array.reshape(source_height, source_width, 4) return array.reshape(source_height, source_width, 4)
def create_final(output_name, pixels_data, final_height, final_width): def create_final(output_name, pixels_data, final_height, final_width):
#print('output_name: ', output_name) #print('output_name: ', output_name)
@@ -60,35 +58,23 @@ def create_final(output_name, pixels_data, final_height, final_width):
return new_img return new_img
def guess_input_format(img_list): def guess_input_format(img_list):
for i in img_list: for i in img_list:
if i.size[0] == i.size[1]: if i.size[0] == i.size[1]:
return i.size return i.size
def format_files(files, catalog_data): def format_files(files, catalog_data):
img_dict = {} img_dict = {}
for k, v in catalog_data.items(): for k, v in catalog_data.items():
if "/" not in k: if '/' not in k:
continue continue
img_dict[v["name"]] = [f for f in files if v["name"] in f] img_dict[v['name']] = [f for f in files if v['name'] in f]
return img_dict return img_dict
def mosaic_export( def mosaic_export(
files, files, catalog_data, row=2, columns=2, auto_calculate=True,
catalog_data, bg_color=(0.18, 0.18, 0.18,), resize_output=100,
row=2,
columns=2,
auto_calculate=True,
bg_color=(
0.18,
0.18,
0.18,
),
resize_output=100,
): ):
img_dict = format_files(files, catalog_data) img_dict = format_files(files, catalog_data)
@@ -106,21 +92,21 @@ def mosaic_export(
chars = Path(files_list[0]).parts[-4] chars = Path(files_list[0]).parts[-4]
output_dir = str(Path(files_list[0]).parent.parent) output_dir = str(Path(files_list[0]).parent.parent)
ext = "jpg" ext = 'jpg'
output_name = f"{chars}_{cat}.{ext}" output_name = f'{chars}_{cat}.{ext}'
for img in files_list: for img in files_list:
img_list.append(bpy.data.images.load(img, check_existing=True)) img_list.append(bpy.data.images.load(img, check_existing=True))
for i in img_list: for i in img_list:
i.colorspace_settings.name = "Raw" i.colorspace_settings.name = 'Raw'
if auto_calculate: if auto_calculate:
rows = int(math.sqrt(len(img_list))) rows = int(math.sqrt(len(img_list)))
columns = math.ceil(len(img_list)/rows) columns = math.ceil(len(img_list)/rows)
if rows*columns < len(img_list): if rows*columns < len(img_list):
raise AttributeError("Grid too small for number of images") raise AttributeError('Grid too small for number of images')
src_w, src_h = img_list[0].size src_w, src_h = img_list[0].size
final_w = src_w * columns final_w = src_w * columns
@@ -144,20 +130,17 @@ def mosaic_export(
else: else:
combined_stack = np.hstack((h_stack[:])) combined_stack = np.hstack((h_stack[:]))
combined_img = create_final( combined_img = create_final(output_name, combined_stack.flatten(), final_h, final_w)
output_name, combined_stack.flatten(), final_h, final_w
)
if resize_output != 100: if resize_output != 100:
w, h = combined_img.size w, h = combined_img.size
combined_img.scale(w * (resize_output * 0.01), h * (resize_output * 0.01)) combined_img.scale(w*(resize_output*.01), h*(resize_output*.01))
combined_img.filepath_raw = "/".join([output_dir, output_name])
combined_img.file_format = "JPEG" combined_img.filepath_raw = '/'.join([output_dir, output_name])
combined_img.file_format = 'JPEG'
combined_img.save() combined_img.save()
print( print(f"""
f"""
Image saved: {combined_img.filepath_raw} Image saved: {combined_img.filepath_raw}
""" """)
)
@@ -13,7 +13,15 @@ import functools
import re import re
import bpy import bpy
from bpy.types import Action, Bone, Context, FCurve, Keyframe, Object, TimelineMarker from bpy.types import (
Action,
Bone,
Context,
FCurve,
Keyframe,
Object,
TimelineMarker
)
from asset_library.common.bl_utils import active_catalog_id, split_path from asset_library.common.bl_utils import active_catalog_id, split_path
@@ -22,7 +30,6 @@ FCurveValue = Union[float, int]
pose_bone_re = re.compile(r'pose.bones\["([^"]+)"\]') pose_bone_re = re.compile(r'pose.bones\["([^"]+)"\]')
"""RegExp for matching FCurve data paths.""" """RegExp for matching FCurve data paths."""
def is_pose(action): def is_pose(action):
for fc in action.fcurves: for fc in action.fcurves:
if len(fc.keyframe_points) > 1: if len(fc.keyframe_points) > 1:
@@ -30,7 +37,6 @@ def is_pose(action):
return True return True
def get_bone_visibility(data_path): def get_bone_visibility(data_path):
bone, prop = split_path(data_path) bone, prop = split_path(data_path)
@@ -41,7 +47,6 @@ def get_bone_visibility(data_path):
return ob.data.layers[b_layers[0]] return ob.data.layers[b_layers[0]]
def get_keyframes(action, selected=False, includes=[]): def get_keyframes(action, selected=False, includes=[]):
if selected: if selected:
# keyframes = sorted([int(k.co[0]) for f in action.fcurves for k in f.keyframe_points if k.select_control_point and get_bone_visibility(f.data_path)]) # keyframes = sorted([int(k.co[0]) for f in action.fcurves for k in f.keyframe_points if k.select_control_point and get_bone_visibility(f.data_path)])
@@ -60,21 +65,14 @@ def get_keyframes(action, selected=False, includes=[]):
if len(keyframes) <= 1: if len(keyframes) <= 1:
keyframes = [bpy.context.scene.frame_current] keyframes = [bpy.context.scene.frame_current]
else: else:
keyframes = sorted( keyframes = sorted([int(k.co[0]) for f in action.fcurves for k in f.keyframe_points])
[int(k.co[0]) for f in action.fcurves for k in f.keyframe_points]
)
return keyframes return keyframes
def get_marker(action): def get_marker(action):
if action.pose_markers: if action.pose_markers:
markers = action.pose_markers markers = action.pose_markers
return next( return next((m.name for m in markers if m.frame == bpy.context.scene.frame_current), None)
(m.name for m in markers if m.frame == bpy.context.scene.frame_current),
None,
)
def reset_bone(bone, transform=True, custom_props=True): def reset_bone(bone, transform=True, custom_props=True):
if transform: if transform:
@@ -97,32 +95,29 @@ def reset_bone(bone, transform=True, custom_props=True):
if not isinstance(value, (int, float)) or not id_prop: if not isinstance(value, (int, float)) or not id_prop:
continue continue
bone[key] = id_prop.as_dict()["default"] bone[key] = id_prop.as_dict()['default']
def is_asset_action(action): def is_asset_action(action):
return action.asset_data and action.asset_data.catalog_id != str(uuid.UUID(int=0)) return action.asset_data and action.asset_data.catalog_id != str(uuid.UUID(int=0))
def conform_action(action): def conform_action(action):
tags = ("pose", "anim") tags = ('pose', 'anim')
if any(tag in action.asset_data.tags.keys() for tag in tags): if any(tag in action.asset_data.tags.keys() for tag in tags):
return return
for fc in action.fcurves: for fc in action.fcurves:
action.asset_data["is_single_frame"] = True action.asset_data['is_single_frame'] = True
if len(fc.keyframe_points) > 1: if len(fc.keyframe_points) > 1:
action.asset_data["is_single_frame"] = False action.asset_data['is_single_frame'] = False
break break
if action.asset_data["is_single_frame"]: if action.asset_data['is_single_frame']:
action.asset_data.tags.new("pose") action.asset_data.tags.new('pose')
else: else:
action.asset_data.tags.new("anim") action.asset_data.tags.new('anim')
def clean_action(action='', frame_start=0, frame_end=0, excludes=[], includes=[]):
def clean_action(action="", frame_start=0, frame_end=0, excludes=[], includes=[]):
## Clean Keyframe Before/After Range ## Clean Keyframe Before/After Range
for fc in action.fcurves: for fc in action.fcurves:
bone, prop = split_path(fc.data_path) bone, prop = split_path(fc.data_path)
@@ -143,16 +138,14 @@ def clean_action(action="", frame_start=0, frame_end=0, excludes=[], includes=[]
fc.keyframe_points.remove(k) fc.keyframe_points.remove(k)
fc.update() fc.update()
def append_action(action_path='', action_name=''):
def append_action(action_path="", action_name=""): print(f'Loading {action_name} from: {action_path}')
print(f"Loading {action_name} from: {action_path}")
with bpy.data.libraries.load(str(action_path), link=False) as (data_from, data_to): with bpy.data.libraries.load(str(action_path), link=False) as (data_from, data_to):
data_to.actions = [action_name] data_to.actions = [action_name]
return data_to.actions[0] return data_to.actions[0]
def apply_anim(action_lib, ob, bones=[]): def apply_anim(action_lib, ob, bones=[]):
from mathutils import Vector from mathutils import Vector
@@ -169,23 +162,14 @@ def apply_anim(action_lib, ob, bones=[]):
keys = sorted([k.co[0] for f in action_lib.fcurves for k in f.keyframe_points]) keys = sorted([k.co[0] for f in action_lib.fcurves for k in f.keyframe_points])
if not keys: if not keys:
print(f"The action {action_lib.name} has no keyframes") print(f'The action {action_lib.name} has no keyframes')
return return
first_key = keys[0] first_key = keys[0]
key_offset = scn.frame_current - first_key key_offset = scn.frame_current - first_key
key_attr = ( key_attr = ('type', 'interpolation', 'handle_left_type', 'handle_right_type',
"type", 'amplitude', 'back', 'easing', 'period', 'handle_right', 'handle_left'
"interpolation",
"handle_left_type",
"handle_right_type",
"amplitude",
"back",
"easing",
"period",
"handle_right",
"handle_left",
) )
for fc in action_lib.fcurves: for fc in action_lib.fcurves:
bone_name, prop_name = split_path(fc.data_path) bone_name, prop_name = split_path(fc.data_path)
@@ -198,16 +182,17 @@ def apply_anim(action_lib, ob, bones=[]):
action_fc = action.fcurves.new( action_fc = action.fcurves.new(
fc.data_path, fc.data_path,
index=fc.array_index, index=fc.array_index,
action_group=fc.group.name if fc.group else fc.data_path.split('"')[1], action_group=fc.group.name if fc.group else fc.data_path.split('"')[1]
) )
for kf_lib in fc.keyframe_points: for kf_lib in fc.keyframe_points:
kf = action_fc.keyframe_points.insert( kf = action_fc.keyframe_points.insert(
frame=kf_lib.co[0] + key_offset, value=kf_lib.co[1] frame=kf_lib.co[0] + key_offset,
value=kf_lib.co[1]
) )
for attr in key_attr: for attr in key_attr:
src_val = getattr(kf_lib, attr) src_val = getattr(kf_lib, attr)
if attr.startswith("handle") and "type" not in attr: if attr.startswith('handle') and 'type' not in attr:
src_val += Vector((key_offset, 0)) src_val += Vector((key_offset, 0))
setattr(kf, attr, src_val) setattr(kf, attr, src_val)
@@ -218,5 +203,5 @@ def apply_anim(action_lib, ob, bones=[]):
for window in bpy.context.window_manager.windows: for window in bpy.context.window_manager.windows:
screen = window.screen screen = window.screen
for area in screen.areas: for area in screen.areas:
if area.type == "GRAPH_EDITOR": if area.type == 'GRAPH_EDITOR':
area.tag_redraw() area.tag_redraw()
+49
View File
@@ -0,0 +1,49 @@
import bpy
def draw_context_menu(layout):
params = bpy.context.space_data.params
asset = bpy.context.asset_file_handle
layout.operator("assetlib.open_blend", text="Open blend file")#.asset = asset.name
layout.operator("assetlib.play_preview", text="Play Preview")
layout.separator()
layout.operator_context = 'INVOKE_DEFAULT'
#layout.operator("assetlib.rename_asset", text="Rename Action")
layout.operator("assetlib.remove_assets", text="Remove Assets")
layout.operator("assetlib.edit_data", text="Edit Asset data")
#layout.operator("actionlib.clear_asset", text="Clear Asset (Fake User)").use_fake_user = True
layout.separator()
layout.operator("actionlib.apply_selected_action", text="Apply Pose").flipped = False
layout.operator("actionlib.apply_selected_action", text="Apply Pose (Flipped)").flipped = True
layout.separator()
layout.operator("poselib.blend_pose_asset_for_keymap", text="Blend Pose").flipped = False
layout.operator("poselib.blend_pose_asset_for_keymap", text="Blend Pose (Flipped)").flipped = True
layout.separator()
layout.operator("poselib.pose_asset_select_bones", text="Select Bones").selected_side = 'CURRENT'
layout.operator("poselib.pose_asset_select_bones", text="Select Bones (Flipped)").selected_side = 'FLIPPED'
layout.operator("poselib.pose_asset_select_bones", text="Select Bones (Both)").selected_side = 'BOTH'
layout.separator()
#layout.operator("asset.library_refresh")
if params.display_type == 'THUMBNAIL':
layout.prop_menu_enum(params, "display_size")
def draw_header(layout):
'''Draw the header of the Asset Browser Window'''
layout.separator()
layout.operator("actionlib.store_anim_pose", text='Add Action', icon='FILE_NEW')
@@ -1,10 +1,11 @@
from typing import List, Tuple from typing import List, Tuple
import bpy import bpy
addon_keymaps: List[Tuple[bpy.types.KeyMap, bpy.types.KeyMapItem]] = [] addon_keymaps: List[Tuple[bpy.types.KeyMap, bpy.types.KeyMapItem]] = []
def register(): def register():
wm = bpy.context.window_manager wm = bpy.context.window_manager
addon = wm.keyconfigs.addon addon = wm.keyconfigs.addon
@@ -14,49 +15,34 @@ def register():
km = addon.keymaps.new(name="File Browser Main", space_type="FILE_BROWSER") km = addon.keymaps.new(name="File Browser Main", space_type="FILE_BROWSER")
# DblClick to apply pose. # DblClick to apply pose.
kmi = km.keymap_items.new( kmi = km.keymap_items.new("actionlib.apply_selected_action", "LEFTMOUSE", "DOUBLE_CLICK")
"actionlib.apply_selected_action", "LEFTMOUSE", "DOUBLE_CLICK"
)
kmi.properties.flipped = False kmi.properties.flipped = False
addon_keymaps.append((km, kmi)) addon_keymaps.append((km, kmi))
kmi = km.keymap_items.new( kmi = km.keymap_items.new("actionlib.apply_selected_action", "LEFTMOUSE", "DOUBLE_CLICK", alt=True)
"actionlib.apply_selected_action", "LEFTMOUSE", "DOUBLE_CLICK", alt=True
)
kmi.properties.flipped = True kmi.properties.flipped = True
addon_keymaps.append((km, kmi)) addon_keymaps.append((km, kmi))
kmi = km.keymap_items.new( kmi = km.keymap_items.new("poselib.blend_pose_asset_for_keymap", "LEFTMOUSE", "DOUBLE_CLICK", shift=True)
"poselib.blend_pose_asset_for_keymap", "LEFTMOUSE", "DOUBLE_CLICK", shift=True
)
kmi.properties.flipped = False kmi.properties.flipped = False
addon_keymaps.append((km, kmi)) addon_keymaps.append((km, kmi))
kmi = km.keymap_items.new( kmi = km.keymap_items.new("poselib.blend_pose_asset_for_keymap", "LEFTMOUSE", "DOUBLE_CLICK", alt=True, shift=True)
"poselib.blend_pose_asset_for_keymap",
"LEFTMOUSE",
"DOUBLE_CLICK",
alt=True,
shift=True,
)
kmi.properties.flipped = True kmi.properties.flipped = True
addon_keymaps.append((km, kmi)) addon_keymaps.append((km, kmi))
kmi = km.keymap_items.new("poselib.pose_asset_select_bones", "S", "PRESS") kmi = km.keymap_items.new("poselib.pose_asset_select_bones", "S", "PRESS")
kmi.properties.selected_side = "CURRENT" kmi.properties.selected_side = 'CURRENT'
addon_keymaps.append((km, kmi)) addon_keymaps.append((km, kmi))
kmi = km.keymap_items.new("poselib.pose_asset_select_bones", "S", "PRESS", alt=True) kmi = km.keymap_items.new("poselib.pose_asset_select_bones", "S", "PRESS", alt=True)
kmi.properties.selected_side = "FLIPPED" kmi.properties.selected_side = 'FLIPPED'
addon_keymaps.append((km, kmi)) addon_keymaps.append((km, kmi))
kmi = km.keymap_items.new( kmi = km.keymap_items.new("poselib.pose_asset_select_bones", "S", "PRESS", alt=True, ctrl=True)
"poselib.pose_asset_select_bones", "S", "PRESS", alt=True, ctrl=True kmi.properties.selected_side = 'BOTH'
)
kmi.properties.selected_side = "BOTH"
addon_keymaps.append((km, kmi)) addon_keymaps.append((km, kmi))
def unregister(): def unregister():
for km, kmi in addon_keymaps: for km, kmi in addon_keymaps:
km.keymap_items.remove(kmi) km.keymap_items.remove(kmi)
@@ -26,43 +26,33 @@ from pprint import pprint
from asset_library.pose.pose_creation import( from asset_library.pose.pose_creation import(
create_pose_asset_from_context, create_pose_asset_from_context,
assign_from_asset_browser, assign_from_asset_browser
) )
from asset_library.pose.pose_usage import select_bones, flip_side_name from asset_library.pose.pose_usage import(
select_bones,
flip_side_name
)
from asset_library.action.functions import ( from asset_library.data_type.action.functions import(
apply_anim, apply_anim,
append_action, append_action,
clean_action, clean_action,
reset_bone, reset_bone,
is_asset_action, is_asset_action,
conform_action, conform_action
) )
from bpy.props import ( from bpy.props import (BoolProperty, CollectionProperty, EnumProperty,
BoolProperty, PointerProperty, StringProperty, IntProperty)
CollectionProperty,
EnumProperty,
PointerProperty,
StringProperty,
IntProperty,
)
from bpy.types import ( from bpy.types import (Action, Context, Event, FileSelectEntry, Object,
Action, Operator, PropertyGroup)
Context,
Event,
FileSelectEntry,
Object,
Operator,
PropertyGroup,
)
from bpy_extras import asset_utils from bpy_extras import asset_utils
from bpy_extras.io_utils import ExportHelper, ImportHelper from bpy_extras.io_utils import ExportHelper, ImportHelper
from asset_library.action.functions import ( from asset_library.data_type.action.functions import (
is_pose, is_pose,
get_marker, get_marker,
get_keyframes, get_keyframes,
@@ -77,7 +67,7 @@ from asset_library.common.functions import (
#resync_lib, #resync_lib,
get_active_library, get_active_library,
get_active_catalog, get_active_catalog,
asset_warning_callback, asset_warning_callback
) )
from asset_library.common.bl_utils import ( from asset_library.common.bl_utils import (
@@ -92,7 +82,7 @@ from asset_library.common.bl_utils import (
#load_assets_from, #load_assets_from,
get_asset_space_params, get_asset_space_params,
get_bl_cmd, get_bl_cmd,
get_overriden_col, get_overriden_col
) )
from asset_library.common.file_utils import ( from asset_library.common.file_utils import (
@@ -134,17 +124,17 @@ class ACTIONLIB_OT_restore_previous_action(Operator):
self._timer = wm.event_timer_add(0.001, window=context.window) self._timer = wm.event_timer_add(0.001, window=context.window)
wm.modal_handler_add(self) wm.modal_handler_add(self)
return {"RUNNING_MODAL"} return {'RUNNING_MODAL'}
def modal(self, context, event): def modal(self, context, event):
if event.type != "TIMER": if event.type != 'TIMER':
return {"RUNNING_MODAL"} return {'RUNNING_MODAL'}
wm = context.window_manager wm = context.window_manager
wm.event_timer_remove(self._timer) wm.event_timer_remove(self._timer)
context.object.pose.apply_pose_from_action(self.pose_action) context.object.pose.apply_pose_from_action(self.pose_action)
return {"FINISHED"} return {'FINISHED'}
class ACTIONLIB_OT_assign_action(Operator): class ACTIONLIB_OT_assign_action(Operator):
@@ -168,8 +158,8 @@ class ACTIONLIB_OT_assign_action(Operator):
class ACTIONLIB_OT_replace_pose(Operator): class ACTIONLIB_OT_replace_pose(Operator):
bl_idname = "actionlib.replace_pose" bl_idname = "actionlib.replace_pose"
bl_options = {"REGISTER", "UNDO", "INTERNAL"} bl_options = {"REGISTER", "UNDO", "INTERNAL"}
bl_label = "Update Pose" bl_label = 'Update Pose'
bl_description = "Update selected Pose. ! Works only on Pose, not Anim !" bl_description = 'Update selected Pose. ! Works only on Pose, not Anim !'
@classmethod @classmethod
def poll(cls, context: Context) -> bool: def poll(cls, context: Context) -> bool:
@@ -180,11 +170,7 @@ class ACTIONLIB_OT_replace_pose(Operator):
# else: # else:
# cls.poll_message_set(f"Current Action {context.id.name} different than Edit Action {wm.edit_pose_action}") # cls.poll_message_set(f"Current Action {context.id.name} different than Edit Action {wm.edit_pose_action}")
# return False # return False
if ( if context.mode == 'POSE' and context.area.ui_type == 'ASSETS' and context.active_file:
context.mode == "POSE"
and context.area.ui_type == "ASSETS"
and context.active_file
):
return True return True
def execute(self, context: Context) -> Set[str]: def execute(self, context: Context) -> Set[str]:
@@ -195,13 +181,12 @@ class ACTIONLIB_OT_replace_pose(Operator):
select_bones( select_bones(
context.object, context.object,
context.asset_file_handle.local_id, context.asset_file_handle.local_id,
selected_side="BOTH", selected_side='BOTH',
toggle=False, toggle=False)
)
data = { data = {
"name": active.name, 'name':active.name,
"catalog_id": active.asset_data.catalog_id, 'catalog_id':active.asset_data.catalog_id,
} }
data.update(dict(active.asset_data)) data.update(dict(active.asset_data))
@@ -214,7 +199,7 @@ class ACTIONLIB_OT_replace_pose(Operator):
action = create_pose_asset_from_context( action = create_pose_asset_from_context(
context, context,
data["name"], data['name'],
) )
if not action: if not action:
self.report( # type: ignore self.report( # type: ignore
@@ -228,36 +213,33 @@ class ACTIONLIB_OT_replace_pose(Operator):
_old_action.use_fake_user = False _old_action.use_fake_user = False
bpy.data.actions.remove(_old_action) bpy.data.actions.remove(_old_action)
action.name = data["name"] action.name = data['name']
action.asset_data.catalog_id = data["catalog_id"] action.asset_data.catalog_id = data['catalog_id']
for k, v in data.items(): for k, v in data.items():
if k in ("camera", "is_single_frame"): if k in ('camera', 'is_single_frame'):
action.asset_data[k] = v action.asset_data[k] = v
if ( if not is_pose(action) and 'pose' in action.asset_data.tags.keys() and 'anim' not in action.asset_data.tags.keys():
not is_pose(action)
and "pose" in action.asset_data.tags.keys()
and "anim" not in action.asset_data.tags.keys()
):
for tag in action.asset_data.tags: for tag in action.asset_data.tags:
if tag != "pose": if tag != 'pose':
continue continue
action.asset_data.tags.remove(tag) action.asset_data.tags.remove(tag)
action.asset_data.tags.new("anim") action.asset_data.tags.new('anim')
return {"FINISHED"}
return {'FINISHED'}
class ACTIONLIB_OT_apply_anim(Operator): class ACTIONLIB_OT_apply_anim(Operator):
bl_idname = "actionlib.apply_anim" bl_idname = "actionlib.apply_anim"
bl_options = {"REGISTER", "UNDO", "INTERNAL"} bl_options = {"REGISTER", "UNDO", "INTERNAL"}
bl_label = "Apply Anim" bl_label = 'Apply Anim'
bl_description = "Apply selected Anim to selected bones" bl_description = 'Apply selected Anim to selected bones'
@classmethod @classmethod
def poll(cls, context: Context) -> bool: def poll(cls, context: Context) -> bool:
return context.mode == "POSE" return context.mode == 'POSE'
def execute(self, context: Context) -> Set[str]: def execute(self, context: Context) -> Set[str]:
ob = context.object ob = context.object
@@ -268,20 +250,20 @@ class ACTIONLIB_OT_apply_anim(Operator):
#params = get_asset_space_params(context.area) #params = get_asset_space_params(context.area)
asset_library_ref = context.asset_library_ref asset_library_ref = context.asset_library_ref
if asset_library_ref == "LOCAL": if asset_library_ref == 'LOCAL':
action = bpy.data.actions[active_action.name] action = bpy.data.actions[active_action.name]
to_remove = False to_remove = False
else: else:
asset_file_handle = bpy.context.asset_file_handle asset_file_handle = bpy.context.asset_file_handle
if asset_file_handle is None: if asset_file_handle is None:
return {"CANCELLED"} return {'CANCELLED'}
if asset_file_handle.local_id: if asset_file_handle.local_id:
return {"CANCELLED"} return {'CANCELLED'}
lib = get_active_library() lib = get_active_library()
if "filepath" in asset_file_handle.asset_data: if 'filepath' in asset_file_handle.asset_data:
action_path = asset_file_handle.asset_data["filepath"] action_path = asset_file_handle.asset_data['filepath']
action_path = lib.library_type.format_path(action_path) action_path = lib.library_type.format_path(action_path)
else: else:
action_path = bpy.types.AssetHandle.get_full_library_path( action_path = bpy.types.AssetHandle.get_full_library_path(
@@ -295,9 +277,7 @@ class ACTIONLIB_OT_apply_anim(Operator):
) )
to_remove = True to_remove = True
else: else:
self.report( self.report({"WARNING"}, f"Could not load action path {action_path} not exist")
{"WARNING"}, f"Could not load action path {action_path} not exist"
)
return {"CANCELLED"} return {"CANCELLED"}
bones = [b.name for b in context.selected_pose_bones_from_active_object] bones = [b.name for b in context.selected_pose_bones_from_active_object]
@@ -305,7 +285,7 @@ class ACTIONLIB_OT_apply_anim(Operator):
if to_remove: if to_remove:
bpy.data.actions.remove(action) bpy.data.actions.remove(action)
return {"FINISHED"} return {'FINISHED'}
""" """
@@ -484,10 +464,8 @@ class ACTIONLIB_OT_create_anim_asset(Operator):
return True return True
params = get_asset_space_params(asset_browse_area) params = get_asset_space_params(asset_browse_area)
if params.asset_library_ref != "LOCAL": if params.asset_library_ref != 'LOCAL':
cls.poll_message_set( cls.poll_message_set("Asset Browser must be set to the Current File library")
"Asset Browser must be set to the Current File library"
)
return False return False
return True return True
@@ -507,71 +485,67 @@ class ACTIONLIB_OT_create_anim_asset(Operator):
data_dict = dict( data_dict = dict(
is_single_frame=False, is_single_frame=False,
camera=context.scene.camera.name if context.scene.camera else "", camera= context.scene.camera.name if context.scene.camera else '',
) )
data.tags.new("anim") data.tags.new('anim')
for k, v in data_dict.items(): for k, v in data_dict.items():
data[k] = v data[k] = v
### ###
return {"FINISHED"} return {'FINISHED'}
class ACTIONLIB_OT_apply_selected_action(Operator): class ACTIONLIB_OT_apply_selected_action(Operator):
bl_idname = "actionlib.apply_selected_action" bl_idname = "actionlib.apply_selected_action"
bl_options = {"REGISTER", "UNDO", "INTERNAL"} bl_options = {"REGISTER", "UNDO", "INTERNAL"}
bl_label = "Apply Pose/Anim" bl_label = 'Apply Pose/Anim'
bl_description = "Apply selected Action to selected bones" bl_description = 'Apply selected Action to selected bones'
flipped: BoolProperty(name="Flipped", default=False) # type: ignore flipped: BoolProperty(name="Flipped", default=False) # type: ignore
@classmethod @classmethod
def poll(cls, context: Context) -> bool: def poll(cls, context: Context) -> bool:
if context.mode == "POSE" and context.area.ui_type == "ASSETS": if context.mode == 'POSE' and context.area.ui_type == 'ASSETS':
return True return True
def execute(self, context: Context) -> Set[str]: def execute(self, context: Context) -> Set[str]:
active_action = context.active_file active_action = context.active_file
if "pose" in active_action.asset_data.tags.keys(): if 'pose' in active_action.asset_data.tags.keys():
bpy.ops.poselib.apply_pose_asset_for_keymap(flipped=self.flipped) bpy.ops.poselib.apply_pose_asset_for_keymap(flipped=self.flipped)
else: else:
bpy.ops.actionlib.apply_anim() bpy.ops.actionlib.apply_anim()
return {"FINISHED"} return {'FINISHED'}
class ACTIONLIB_OT_edit_action(Operator): class ACTIONLIB_OT_edit_action(Operator):
bl_idname = "actionlib.edit_action" bl_idname = "actionlib.edit_action"
bl_options = {"REGISTER", "UNDO", "INTERNAL"} bl_options = {"REGISTER", "UNDO", "INTERNAL"}
bl_label = "Edit Action" bl_label = 'Edit Action'
bl_description = "Assign active action and set Camera" bl_description = 'Assign active action and set Camera'
@classmethod @classmethod
def poll(cls, context: Context) -> bool: def poll(cls, context: Context) -> bool:
if ( if context.mode == 'POSE' and context.area.ui_type == 'ASSETS' and context.active_file:
context.mode == "POSE"
and context.area.ui_type == "ASSETS"
and context.active_file
):
return True return True
def execute(self, context: Context) -> Set[str]: def execute(self, context: Context) -> Set[str]:
scn = context.scene scn = context.scene
rest_pose = bpy.data.actions.get(context.id.asset_data.get("rest_pose", "")) rest_pose = bpy.data.actions.get(context.id.asset_data.get('rest_pose', ''))
context.object.animation_data_create() context.object.animation_data_create()
keyframes = get_keyframes(context.id) keyframes = get_keyframes(context.id)
if not keyframes: if not keyframes:
self.report({"ERROR"}, f"No Keyframes found for {context.id.name}.") self.report({'ERROR'}, f'No Keyframes found for {context.id.name}.')
return return
scn.frame_set(keyframes[0]) scn.frame_set(keyframes[0])
context.object.animation_data.action = None context.object.animation_data.action = None
for b in context.object.pose.bones: for b in context.object.pose.bones:
if re.match("^[A-Z]+\.", b.name): if re.match('^[A-Z]+\.', b.name):
continue continue
reset_bone(b) reset_bone(b)
@@ -579,8 +553,8 @@ class ACTIONLIB_OT_edit_action(Operator):
context.view_layer.update() context.view_layer.update()
context.object.animation_data.action = context.id context.object.animation_data.action = context.id
if "camera" in context.id.asset_data.keys(): if 'camera' in context.id.asset_data.keys():
scn.camera = bpy.data.objects[context.id.asset_data["camera"]] scn.camera = bpy.data.objects[context.id.asset_data['camera']]
return {"FINISHED"} return {"FINISHED"}
@@ -588,16 +562,12 @@ class ACTIONLIB_OT_edit_action(Operator):
class ACTIONLIB_OT_clear_action(Operator): class ACTIONLIB_OT_clear_action(Operator):
bl_idname = "actionlib.clear_action" bl_idname = "actionlib.clear_action"
bl_options = {"REGISTER", "UNDO", "INTERNAL"} bl_options = {"REGISTER", "UNDO", "INTERNAL"}
bl_label = "Clear Action" bl_label = 'Clear Action'
bl_description = "Assign active action and set Camera" bl_description = 'Assign active action and set Camera'
@classmethod @classmethod
def poll(cls, context: Context) -> bool: def poll(cls, context: Context) -> bool:
if ( if context.mode == 'POSE' and context.area.ui_type == 'ASSETS' and context.active_file:
context.mode == "POSE"
and context.area.ui_type == "ASSETS"
and context.active_file
):
return True return True
def execute(self, context: Context) -> Set[str]: def execute(self, context: Context) -> Set[str]:
@@ -608,7 +578,7 @@ class ACTIONLIB_OT_clear_action(Operator):
context.id.asset_generate_preview() context.id.asset_generate_preview()
for a in context.screen.areas: for a in context.screen.areas:
if a.type == "DOPESHEET_EDITOR" and a.ui_type == "DOPESHEET": if a.type == 'DOPESHEET_EDITOR' and a.ui_type == 'DOPESHEET':
a.tag_redraw() a.tag_redraw()
return {"FINISHED"} return {"FINISHED"}
@@ -617,12 +587,12 @@ class ACTIONLIB_OT_clear_action(Operator):
class ACTIONLIB_OT_generate_preview(Operator): class ACTIONLIB_OT_generate_preview(Operator):
bl_idname = "actionlib.generate_preview" bl_idname = "actionlib.generate_preview"
bl_options = {"REGISTER", "UNDO", "INTERNAL"} bl_options = {"REGISTER", "UNDO", "INTERNAL"}
bl_label = "Generate Preview" bl_label = 'Generate Preview'
bl_description = "Genreate Preview for Active Action" bl_description = 'Genreate Preview for Active Action'
@classmethod @classmethod
def poll(cls, context: Context) -> bool: def poll(cls, context: Context) -> bool:
if context.object.type == "ARMATURE" and context.mode == "POSE": if context.object.type == 'ARMATURE' and context.mode == 'POSE':
return True return True
def execute(self, context: Context) -> Set[str]: def execute(self, context: Context) -> Set[str]:
@@ -633,18 +603,18 @@ class ACTIONLIB_OT_generate_preview(Operator):
actions = context.selected_files + [_action] actions = context.selected_files + [_action]
for a in context.selected_files: for a in context.selected_files:
rest_pose = bpy.data.actions.get(a.asset_data.get("rest_pose", "")) rest_pose = bpy.data.actions.get(a.asset_data.get('rest_pose', ''))
if rest_pose: if rest_pose:
context.object.animation_data.action = rest_pose context.object.animation_data.action = rest_pose
bpy.context.view_layer.update() bpy.context.view_layer.update()
else: else:
context.object.animation_data.action = None context.object.animation_data.action = None
for b in context.object.pose.bones: for b in context.object.pose.bones:
if re.match("^[A-Z]+\.", b.name): if re.match('^[A-Z]+\.', b.name):
continue continue
reset_bone(b) reset_bone(b)
a_cam = bpy.data.objects.get(a.asset_data["camera"]) a_cam = bpy.data.objects.get(a.asset_data['camera'])
if a_cam: if a_cam:
context.scene.camera = a_cam context.scene.camera = a_cam
bpy.context.view_layer.update() bpy.context.view_layer.update()
@@ -661,15 +631,15 @@ class ACTIONLIB_OT_generate_preview(Operator):
class ACTIONLIB_OT_update_action_data(Operator): class ACTIONLIB_OT_update_action_data(Operator):
bl_idname = "actionlib.update_action_data" bl_idname = "actionlib.update_action_data"
bl_options = {"REGISTER", "INTERNAL"} bl_options = {"REGISTER", "INTERNAL"}
bl_label = "Udpate Action Data" bl_label = 'Udpate Action Data'
bl_description = "Update Action Metadata" bl_description = 'Update Action Metadata'
tags: EnumProperty( tags: EnumProperty(
name="Tags", name='Tags',
items=( items=(
("POSE", "pose", ""), ('POSE', "pose", ""),
("ANIM", "anim", ""), ('ANIM', "anim", ""),
), )
) )
use_tags: BoolProperty(default=False) use_tags: BoolProperty(default=False)
@@ -705,7 +675,7 @@ class ACTIONLIB_OT_update_action_data(Operator):
row.prop(self, "use_camera", text="") row.prop(self, "use_camera", text="")
sub = row.row() sub = row.row()
sub.active = self.use_camera sub.active = self.use_camera
sub.prop(scn.actionlib, "camera", text="", icon="CAMERA_DATA") sub.prop(scn.actionlib, "camera", text="", icon='CAMERA_DATA')
heading = layout.column(align=True, heading="Rest Pose") heading = layout.column(align=True, heading="Rest Pose")
row = heading.row(align=True) row = heading.row(align=True)
@@ -719,11 +689,11 @@ class ACTIONLIB_OT_update_action_data(Operator):
for action in context.selected_files: for action in context.selected_files:
if self.use_camera: if self.use_camera:
action.asset_data["camera"] = context.scene.actionlib.camera.name action.asset_data['camera'] = context.scene.actionlib.camera.name
if self.use_tags: if self.use_tags:
tag = self.tags.lower() tag = self.tags.lower()
not_tag = "anim" if tag == "pose" else "pose" not_tag = 'anim' if tag == 'pose' else 'pose'
if tag not in action.asset_data.tags.keys(): if tag not in action.asset_data.tags.keys():
if not_tag in action.asset_data.tags.keys(): if not_tag in action.asset_data.tags.keys():
@@ -734,27 +704,29 @@ class ACTIONLIB_OT_update_action_data(Operator):
action.asset_data.tags.new(tag) action.asset_data.tags.new(tag)
if "pose" in action.asset_data.tags.keys(): if 'pose' in action.asset_data.tags.keys():
action.asset_data["is_single_frame"] = True action.asset_data['is_single_frame'] = True
else: else:
action.asset_data["is_single_frame"] = False action.asset_data['is_single_frame'] = False
if self.use_rest_pose: if self.use_rest_pose:
name = scn.actionlib.rest_pose.name if scn.actionlib.rest_pose else "" name = scn.actionlib.rest_pose.name if scn.actionlib.rest_pose else ''
action.asset_data["rest_pose"] = name action.asset_data['rest_pose'] = name
return {'FINISHED'}
return {"FINISHED"}
class ACTIONLIB_OT_assign_rest_pose(Operator): class ACTIONLIB_OT_assign_rest_pose(Operator):
bl_idname = "actionlib.mark_as_rest_pose" bl_idname = "actionlib.mark_as_rest_pose"
bl_options = {"REGISTER", "UNDO", "INTERNAL"} bl_options = {"REGISTER", "UNDO", "INTERNAL"}
bl_label = "Mark As Rest Pose" bl_label = 'Mark As Rest Pose'
bl_description = "Mark Pose as Rest Pose" bl_description = 'Mark Pose as Rest Pose'
@classmethod @classmethod
def poll(cls, context: Context) -> bool: def poll(cls, context: Context) -> bool:
if context.area.ui_type == "ASSETS": if context.area.ui_type == 'ASSETS':
return True return True
def execute(self, context: Context) -> Set[str]: def execute(self, context: Context) -> Set[str]:
@@ -762,14 +734,14 @@ class ACTIONLIB_OT_assign_rest_pose(Operator):
context.scene.actionlib.rest_pose = context.id context.scene.actionlib.rest_pose = context.id
print(f"'{active_action.name.title()}' marked as Rest Pose.") print(f"'{active_action.name.title()}' marked as Rest Pose.")
return {"FINISHED"} return {'FINISHED'}
class ACTIONLIB_OT_open_blendfile(Operator): class ACTIONLIB_OT_open_blendfile(Operator):
bl_idname = "actionlib.open_blendfile" bl_idname = "actionlib.open_blendfile"
bl_options = {"REGISTER", "UNDO", "INTERNAL"} bl_options = {"REGISTER", "UNDO", "INTERNAL"}
bl_label = "Set Paths" bl_label = 'Set Paths'
bl_description = "Open Containing File" bl_description = 'Open Containing File'
replace_local : BoolProperty(default=False) replace_local : BoolProperty(default=False)
@@ -779,7 +751,7 @@ class ACTIONLIB_OT_open_blendfile(Operator):
return False return False
sp = context.space_data sp = context.space_data
if sp.params.asset_library_ref == "LOCAL": if sp.params.asset_library_ref == 'LOCAL':
return False return False
return True return True
@@ -792,11 +764,13 @@ class ACTIONLIB_OT_open_blendfile(Operator):
blendfile=str(asset_path), blendfile=str(asset_path),
) )
subprocess.Popen(cmd) subprocess.Popen(cmd)
return {"FINISHED"} return {'FINISHED'}
#LIBRARY_ITEMS = [] #LIBRARY_ITEMS = []
""" '''
def callback_operator(modal_func, operator, override={}): def callback_operator(modal_func, operator, override={}):
def wrap(self, context, event): def wrap(self, context, event):
@@ -808,7 +782,7 @@ def callback_operator(modal_func, operator, override={}):
return retset return retset
return wrap return wrap
""" '''
class ACTIONLIB_OT_make_custom_preview(Operator): class ACTIONLIB_OT_make_custom_preview(Operator):
@@ -820,47 +794,34 @@ class ACTIONLIB_OT_make_custom_preview(Operator):
prefs = get_addons_prefs() prefs = get_addons_prefs()
if not prefs.preview_modal: if not prefs.preview_modal:
with context.temp_override( with context.temp_override(area=self.source_area, region=self.source_area.regions[-1]):
area=self.source_area, region=self.source_area.regions[-1] bpy.ops.actionlib.store_anim_pose("INVOKE_DEFAULT", clear_previews=False, **prefs.add_asset_dict)
):
bpy.ops.actionlib.store_anim_pose(
"INVOKE_DEFAULT", clear_previews=False, **prefs.add_asset_dict
)
return {"FINISHED"} return {"FINISHED"}
return {"PASS_THROUGH"} return {'PASS_THROUGH'}
def invoke(self, context, event): def invoke(self, context, event):
self.source_area = bpy.context.area self.source_area = bpy.context.area
view3d = get_viewport() view3d = get_viewport()
with context.temp_override( with context.temp_override(area=view3d, region=view3d.regions[-1], window=context.window):
area=view3d, region=view3d.regions[-1], window=context.window
):
# To close the popup # To close the popup
bpy.ops.screen.screen_full_area() bpy.ops.screen.screen_full_area()
bpy.ops.screen.back_to_previous() bpy.ops.screen.back_to_previous()
view3d = get_viewport() view3d = get_viewport()
with context.temp_override( with context.temp_override(area=view3d, region=view3d.regions[-1], window=context.window):
area=view3d, region=view3d.regions[-1], window=context.window bpy.ops.assetlib.make_custom_preview('INVOKE_DEFAULT', modal=True)
):
bpy.ops.assetlib.make_custom_preview("INVOKE_DEFAULT", modal=True)
context.window_manager.modal_handler_add(self) context.window_manager.modal_handler_add(self)
return {"RUNNING_MODAL"} return {'RUNNING_MODAL'}
def get_preview_items(self, context): def get_preview_items(self, context):
prefs = get_addon_prefs() prefs = get_addon_prefs()
return sorted( return sorted([(k, k, '', v.icon_id, index) for index, (k, v) in enumerate(prefs.previews.items())], reverse=True)
[
(k, k, "", v.icon_id, index)
for index, (k, v) in enumerate(prefs.previews.items())
],
reverse=True,
)
class ACTIONLIB_OT_store_anim_pose(Operator): class ACTIONLIB_OT_store_anim_pose(Operator):
@@ -868,40 +829,32 @@ class ACTIONLIB_OT_store_anim_pose(Operator):
bl_label = "Add Action to the current library" bl_label = "Add Action to the current library"
bl_description = "Store current pose/anim to local library" bl_description = "Store current pose/anim to local library"
warning: StringProperty(name="") warning: StringProperty(name='')
path: StringProperty(name="Path") path: StringProperty(name='Path')
catalog: StringProperty( catalog: StringProperty(name='Catalog', update=asset_warning_callback, options={'TEXTEDIT_UPDATE'})
name="Catalog", update=asset_warning_callback, options={"TEXTEDIT_UPDATE"} name: StringProperty(name='Name', update=asset_warning_callback, options={'TEXTEDIT_UPDATE'})
) action_type: EnumProperty(name='Type', items=[('POSE', 'Pose', ''), ('ANIMATION', 'Animation', '')])
name: StringProperty(
name="Name", update=asset_warning_callback, options={"TEXTEDIT_UPDATE"}
)
action_type: EnumProperty(
name="Type", items=[("POSE", "Pose", ""), ("ANIMATION", "Animation", "")]
)
frame_start: IntProperty(name="Frame Start") frame_start: IntProperty(name="Frame Start")
frame_end: IntProperty(name="Frame End") frame_end: IntProperty(name="Frame End")
tags: StringProperty( tags: StringProperty(name='Tags', description='Tags need to separate with a comma (,)')
name="Tags", description="Tags need to separate with a comma (,)" description: StringProperty(name='Description')
)
description: StringProperty(name="Description")
preview : EnumProperty(items=get_preview_items) preview : EnumProperty(items=get_preview_items)
clear_previews : BoolProperty(default=True) clear_previews : BoolProperty(default=True)
store_library: StringProperty(name="Store Library") store_library: StringProperty(name='Store Library')
@classmethod @classmethod
def poll(cls, context: Context) -> bool: def poll(cls, context: Context) -> bool:
ob = context.object ob = context.object
if not ob: if not ob:
cls.poll_message_set(f"You have no active object") cls.poll_message_set(f'You have no active object')
return False return False
if not ob.type == "ARMATURE": if not ob.type == 'ARMATURE':
cls.poll_message_set(f"Active object {ob.name} is not of type ARMATURE") cls.poll_message_set(f'Active object {ob.name} is not of type ARMATURE')
return False return False
if not ob.animation_data or not ob.animation_data.action: if not ob.animation_data or not ob.animation_data.action:
cls.poll_message_set(f"Active object {ob.name} has no action or keyframes") cls.poll_message_set(f'Active object {ob.name} has no action or keyframes')
return False return False
return True return True
@@ -916,16 +869,7 @@ class ACTIONLIB_OT_store_anim_pose(Operator):
# col.operator("asset.tag_remove", icon='REMOVE', text="") # col.operator("asset.tag_remove", icon='REMOVE', text="")
def to_dict(self): def to_dict(self):
keys = ( keys = ("catalog", "name", "action_type", "frame_start", "frame_end", "tags", "description", "store_library")
"catalog",
"name",
"action_type",
"frame_start",
"frame_end",
"tags",
"description",
"store_library",
)
return {k : getattr(self, k) for k in keys} return {k : getattr(self, k) for k in keys}
def draw(self, context): def draw(self, context):
@@ -936,43 +880,41 @@ class ACTIONLIB_OT_store_anim_pose(Operator):
split = layout.split(factor=0.39, align=True) split = layout.split(factor=0.39, align=True)
#row = split.row(align=False) #row = split.row(align=False)
#split.use_property_split = False #split.use_property_split = False
split.alignment = "RIGHT" split.alignment = 'RIGHT'
split.label(text="Preview") split.label(text='Preview')
sub = split.row(align=True) sub = split.row(align=True)
sub.template_icon_view(self, "preview", show_labels=False) sub.template_icon_view(self, "preview", show_labels=False)
sub.separator() sub.separator()
sub.operator( sub.operator("actionlib.make_custom_preview", icon='RESTRICT_RENDER_OFF', text='')
"actionlib.make_custom_preview", icon="RESTRICT_RENDER_OFF", text=""
)
prefs.add_asset_dict.clear() prefs.add_asset_dict.clear()
prefs.add_asset_dict.update(self.to_dict()) prefs.add_asset_dict.update(self.to_dict())
sub.label(icon="BLANK1") sub.label(icon='BLANK1')
#layout.ui_units_x = 50 #layout.ui_units_x = 50
#row = layout.row(align=True) #row = layout.row(align=True)
layout.use_property_split = True layout.use_property_split = True
if self.current_library.merge_libraries: if self.current_library.merge_libraries:
layout.prop(self.current_library, "store_library", expand=False) layout.prop(self.current_library, 'store_library', expand=False)
#row.label(text='Action Name') #row.label(text='Action Name')
layout.prop(self, "action_type", text="Type", expand=True) layout.prop(self, "action_type", text="Type", expand=True)
if self.action_type == "ANIMATION": if self.action_type == 'ANIMATION':
col = layout.column(align=True) col = layout.column(align=True)
col.prop(self, "frame_start") col.prop(self, "frame_start")
col.prop(self, "frame_end", text="End") col.prop(self, "frame_end", text='End')
layout.prop(self, "catalog", text="Catalog") layout.prop(self, "catalog", text="Catalog")
layout.prop(self, "name", text="Name") layout.prop(self, "name", text="Name")
#layout.separator() #layout.separator()
#self.draw_tags(self.asset_action, layout) #self.draw_tags(self.asset_action, layout)
layout.prop(self, "tags") layout.prop(self, 'tags')
layout.prop(self, "description", text="Description") layout.prop(self, 'description', text='Description')
#layout.prop(prefs, 'author', text='Author') #layout.prop(prefs, 'author', text='Author')
#layout.prop() #layout.prop()
@@ -986,7 +928,7 @@ class ACTIONLIB_OT_store_anim_pose(Operator):
col.label(text=self.path) col.label(text=self.path)
if self.warning: if self.warning:
col.label(icon="ERROR", text=self.warning) col.label(icon='ERROR', text=self.warning)
def set_action_type(self): def set_action_type(self):
ob = bpy.context.object ob = bpy.context.object
@@ -998,9 +940,9 @@ class ACTIONLIB_OT_store_anim_pose(Operator):
self.frame_start = min(keyframes_selected) self.frame_start = min(keyframes_selected)
self.frame_end = max(keyframes_selected) self.frame_end = max(keyframes_selected)
self.action_type = "POSE" self.action_type = 'POSE'
if self.frame_start != self.frame_end: if (self.frame_start != self.frame_end):
self.action_type = "ANIMATION" self.action_type = 'ANIMATION'
def invoke(self, context, event): def invoke(self, context, event):
prefs = get_addon_prefs() prefs = get_addon_prefs()
@@ -1020,7 +962,8 @@ class ACTIONLIB_OT_store_anim_pose(Operator):
self.store_library = lib.name self.store_library = lib.name
#lib = self.current_library #lib = self.current_library
self.tags = "" self.tags = ''
#print(self, self.library_items) #print(self, self.library_items)
catalog_item = lib.catalog.active_item catalog_item = lib.catalog.active_item
@@ -1035,7 +978,7 @@ class ACTIONLIB_OT_store_anim_pose(Operator):
view3d = get_viewport() view3d = get_viewport()
with context.temp_override(area=view3d, region=view3d.regions[-1]): with context.temp_override(area=view3d, region=view3d.regions[-1]):
bpy.ops.assetlib.make_custom_preview("INVOKE_DEFAULT") bpy.ops.assetlib.make_custom_preview('INVOKE_DEFAULT')
else: else:
preview_items = get_preview_items(self, context) preview_items = get_preview_items(self, context)
@@ -1056,17 +999,17 @@ class ACTIONLIB_OT_store_anim_pose(Operator):
action=action, action=action,
frame_start=self.frame_start, frame_start=self.frame_start,
frame_end=self.frame_end, frame_end=self.frame_end,
excludes=["world", "walk"], excludes=['world', 'walk'],
includes=bones, includes=bones,
) )
## Define Tags ## Define Tags
tags = [t.strip() for t in self.tags.split(",") if t] tags = [t.strip() for t in self.tags.split(',') if t]
tag_range = f"f{self.frame_start}" tag_range = f'f{self.frame_start}'
is_single_frame = True is_single_frame = True
if self.action_type == "ANIM": if self.action_type == 'ANIM':
is_single_frame = False is_single_frame = False
tag_range = f"f{self.frame_start}-f{self.frame_end}" tag_range = f'f{self.frame_start}-f{self.frame_end}'
tags.append(self.action_type) tags.append(self.action_type)
tags.append(tag_range) tags.append(tag_range)
@@ -1074,15 +1017,15 @@ class ACTIONLIB_OT_store_anim_pose(Operator):
for tag in tags: for tag in tags:
action.asset_data.tags.new(tag) action.asset_data.tags.new(tag)
action.asset_data["is_single_frame"] = is_single_frame action.asset_data['is_single_frame'] = is_single_frame
action.asset_data["rig"] = bpy.context.object.name action.asset_data['rig'] = bpy.context.object.name
action.asset_data.description = self.description action.asset_data.description = self.description
action.asset_data.author = prefs.author action.asset_data.author = prefs.author
col = get_overriden_col(bpy.context.object) col = get_overriden_col(bpy.context.object)
if col: if col:
action.asset_data["col"] = col.name action.asset_data['col'] = col.name
return action return action
@@ -1094,21 +1037,21 @@ class ACTIONLIB_OT_store_anim_pose(Operator):
space = area.spaces.active space = area.spaces.active
attrs = [ attrs = [
(scn, "use_preview_range", True), (scn, 'use_preview_range', True),
(scn, "frame_preview_start", self.frame_start), (scn, 'frame_preview_start', self.frame_start),
(scn, "frame_preview_end", self.frame_end), (scn, 'frame_preview_end', self.frame_end),
(scn.render, "resolution_percentage", 100), (scn.render, 'resolution_percentage', 100),
(space.overlay, "show_overlays", False), (space.overlay, 'show_overlays', False),
(space.region_3d, "view_perspective", "CAMERA"), (space.region_3d, 'view_perspective', 'CAMERA'),
(scn.render, "resolution_x", 1280), (scn.render, 'resolution_x', 1280),
(scn.render, "resolution_y", 720), (scn.render, 'resolution_y', 720),
(scn.render.image_settings, "file_format", "FFMPEG"), (scn.render.image_settings, 'file_format', 'FFMPEG'),
(scn.render.ffmpeg, "format", "QUICKTIME"), (scn.render.ffmpeg, 'format', 'QUICKTIME'),
(scn.render.ffmpeg, "codec", "H264"), (scn.render.ffmpeg, 'codec', 'H264'),
(scn.render.ffmpeg, "ffmpeg_preset", "GOOD"), (scn.render.ffmpeg, 'ffmpeg_preset', 'GOOD'),
(scn.render.ffmpeg, "constant_rate_factor", "HIGH"), (scn.render.ffmpeg, 'constant_rate_factor', 'HIGH'),
(scn.render.ffmpeg, "gopsize", 12), (scn.render.ffmpeg, 'gopsize', 12),
(scn.render, "filepath", str(video_path)), (scn.render, 'filepath', str(video_path)),
] ]
if self.action_type == "ANIMATION": if self.action_type == "ANIMATION":
@@ -1120,7 +1063,7 @@ class ACTIONLIB_OT_store_anim_pose(Operator):
pass pass
def refresh(self, area): def refresh(self, area):
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): def execute(self, context: Context):
@@ -1138,12 +1081,8 @@ class ACTIONLIB_OT_store_anim_pose(Operator):
lib_type = lib.library_type lib_type = lib.library_type
asset_path = lib_type.get_asset_path(name=self.name, catalog=self.catalog) asset_path = lib_type.get_asset_path(name=self.name, catalog=self.catalog)
img_path = lib_type.get_image_path( img_path = lib_type.get_image_path(name=self.name, catalog=self.catalog, filepath=asset_path)
name=self.name, catalog=self.catalog, filepath=asset_path video_path = lib_type.get_video_path(name=self.name, catalog=self.catalog, filepath=asset_path)
)
video_path = lib_type.get_video_path(
name=self.name, catalog=self.catalog, filepath=asset_path
)
## Copy Action ## Copy Action
current_action = ob.animation_data.action current_action = ob.animation_data.action
@@ -1176,7 +1115,7 @@ class ACTIONLIB_OT_store_anim_pose(Operator):
#print('asset_info') #print('asset_info')
#pprint(asset_info) #pprint(asset_info)
diff = [dict(a, operation="ADD") for a in lib_type.flatten_cache([asset_info])] diff = [dict(a, operation='ADD') for a in lib_type.flatten_cache([asset_info])]
ob.animation_data.action = current_action ob.animation_data.action = current_action
@@ -1185,7 +1124,7 @@ class ACTIONLIB_OT_store_anim_pose(Operator):
bpy.data.actions.remove(asset_action) bpy.data.actions.remove(asset_action)
# TODO Write a proper method for this # TODO Write a proper method for this
diff_path = Path(bpy.app.tempdir, "diff.json") diff_path = Path(bpy.app.tempdir, 'diff.json')
#diff = [dict(a, operation='ADD') for a in [asset_info])] #diff = [dict(a, operation='ADD') for a in [asset_info])]
diff_path.write_text(json.dumps(diff, indent=4)) diff_path.write_text(json.dumps(diff, indent=4))
@@ -1194,7 +1133,7 @@ class ACTIONLIB_OT_store_anim_pose(Operator):
#self.area.tag_redraw() #self.area.tag_redraw()
self.report({"INFO"}, f'"{self.name}" has been added to the library.') self.report({'INFO'}, f'"{self.name}" has been added to the library.')
return {"FINISHED"} return {"FINISHED"}
@@ -1213,7 +1152,7 @@ classes = (
ACTIONLIB_OT_update_action_data, ACTIONLIB_OT_update_action_data,
ACTIONLIB_OT_assign_rest_pose, ACTIONLIB_OT_assign_rest_pose,
ACTIONLIB_OT_store_anim_pose, ACTIONLIB_OT_store_anim_pose,
ACTIONLIB_OT_make_custom_preview, ACTIONLIB_OT_make_custom_preview
) )
register, unregister = bpy.utils.register_classes_factory(classes) register, unregister = bpy.utils.register_classes_factory(classes)
@@ -2,19 +2,20 @@ import bpy
from bpy.types import PropertyGroup from bpy.types import PropertyGroup
from bpy.props import PointerProperty, StringProperty, BoolProperty from bpy.props import PointerProperty, StringProperty, BoolProperty
class ACTIONLIB_PG_scene(PropertyGroup): class ACTIONLIB_PG_scene(PropertyGroup):
flipped : BoolProperty( flipped : BoolProperty(
name="Flip Pose", name="Flip Pose",
default=False, default=False,
) )
previous_action : PointerProperty(type=bpy.types.Action) previous_action : PointerProperty(type=bpy.types.Action)
publish_path: StringProperty(subtype="FILE_PATH") publish_path : StringProperty(subtype='FILE_PATH')
camera: PointerProperty(type=bpy.types.Object, poll=lambda s, o: o.type == "CAMERA") camera : PointerProperty(type=bpy.types.Object, poll=lambda s, o: o.type == 'CAMERA')
rest_pose : PointerProperty(type=bpy.types.Action, poll=lambda s, a: a.asset_data) rest_pose : PointerProperty(type=bpy.types.Action, poll=lambda s, a: a.asset_data)
classes = (ACTIONLIB_PG_scene,) classes = (
ACTIONLIB_PG_scene,
)
def register(): def register():
@@ -23,7 +24,6 @@ def register():
bpy.types.Scene.actionlib = PointerProperty(type=ACTIONLIB_PG_scene) bpy.types.Scene.actionlib = PointerProperty(type=ACTIONLIB_PG_scene)
def unregister(): def unregister():
try: try:
del bpy.types.Scene.actionlib del bpy.types.Scene.actionlib
+44
View File
@@ -0,0 +1,44 @@
import argparse
import bpy
import json
import re
import sys
from pathlib import Path
#sys.path.append(str(Path(__file__).parents[3]))
from asset_library.common.bl_utils import (
get_preview,
)
def rename_pose(src_name='', dst_name=''):
scn = bpy.context.scene
action = bpy.data.actions.get(src_name)
if not action:
print(f'No {src_name} not found.')
bpy.ops.wm.quit_blender()
action.name = dst_name
preview = get_preview(asset_path=bpy.data.filepath, asset_name=src_name)
if preview:
preview.rename(re.sub(src_name, dst_name, str(preview)))
bpy.ops.wm.save_mainfile(
filepath=bpy.data.filepath, compress=True, exit=True
)
if __name__ == '__main__' :
parser = argparse.ArgumentParser(description='Add Comment To the tracker',
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument('--src-name')
parser.add_argument('--dst-name')
if '--' in sys.argv :
index = sys.argv.index('--')
sys.argv = [sys.argv[index-1], *sys.argv[index+1:]]
args = parser.parse_args()
rename_pose(**vars(args))
@@ -1,4 +1,5 @@
from asset_library.collection import (
from asset_library.data_type.collection import (
gui, gui,
operators, operators,
keymaps, keymaps,
@@ -6,7 +7,7 @@ from asset_library.collection import (
#create_collection_library, #create_collection_library,
) )
if "bpy" in locals(): if 'bpy' in locals():
import importlib import importlib
importlib.reload(gui) importlib.reload(gui)
@@ -17,12 +18,10 @@ if "bpy" in locals():
import bpy import bpy
def register(): def register():
operators.register() operators.register()
keymaps.register() keymaps.register()
def unregister(): def unregister():
operators.unregister() operators.unregister()
keymaps.unregister() keymaps.unregister()
@@ -29,7 +29,6 @@ from asset_library.constants import ASSETLIB_FILENAME
] ]
""" """
def build_collection_blends(path, categories=None, clean=True): def build_collection_blends(path, categories=None, clean=True):
t0 = time() t0 = time()
@@ -44,33 +43,30 @@ def build_collection_blends(path, categories=None, clean=True):
category_datas = json.loads(json_path.read_text()) category_datas = json.loads(json_path.read_text())
for category_data in category_datas: for category_data in category_datas:
if categories and category_data["name"] not in categories: if categories and category_data['name'] not in categories:
continue continue
bpy.ops.wm.read_homefile(use_empty=True) bpy.ops.wm.read_homefile(use_empty=True)
#category_data = next(c for c in category_datas if c['name'] == category) #category_data = next(c for c in category_datas if c['name'] == category)
#_col_datas = category_data['children'] #_col_datas = category_data['children']
cat_name = category_data["name"] cat_name = category_data['name']
build_path = Path(path) / cat_name / f"{cat_name}.blend" build_path = Path(path) / cat_name / f'{cat_name}.blend'
## re-iterate in grouped filepath ## re-iterate in grouped filepath
col_datas = sorted(category_data["children"], key=lambda x: x["filepath"]) col_datas = sorted(category_data['children'], key=lambda x: x['filepath'])
for filepath, col_data_groups in groupby( for filepath, col_data_groups in groupby(col_datas, key=lambda x: x['filepath']):
col_datas, key=lambda x: x["filepath"]
):
#f = Path(f) #f = Path(f)
if not Path(filepath).exists(): if not Path(filepath).exists():
print(f"Not exists: {filepath}") print(f'Not exists: {filepath}')
continue continue
col_data_groups = list(col_data_groups) col_data_groups = list(col_data_groups)
col_names = [a["name"] for a in col_data_groups] col_names = [a['name'] for a in col_data_groups]
linked_cols = load_datablocks( linked_cols = load_datablocks(filepath, col_names, link=True, type='collections')
filepath, col_names, link=True, type="collections"
)
for i, col in enumerate(linked_cols): for i, col in enumerate(linked_cols):
# iterate in linked collection and associated data # iterate in linked collection and associated data
@@ -82,10 +78,10 @@ def build_collection_blends(path, categories=None, clean=True):
## Directly link as collection inside a marked collection with same name ## Directly link as collection inside a marked collection with same name
marked_col = col_as_asset(col, verbose=True) marked_col = col_as_asset(col, verbose=True)
marked_col.asset_data.description = asset_data.get("description", "") marked_col.asset_data.description = asset_data.get('description', '')
marked_col.asset_data.catalog_id = category_data["id"] # assign catalog marked_col.asset_data.catalog_id = category_data['id'] # assign catalog
for k, v in asset_data.get("metadata", {}).items(): for k, v in asset_data.get('metadata', {}).items():
marked_col.asset_data[k] = v marked_col.asset_data[k] = v
## exclude collections and generate preview ## exclude collections and generate preview
@@ -97,35 +93,31 @@ def build_collection_blends(path, categories=None, clean=True):
## clear all objects (can be very long with a lot of objects...): ## clear all objects (can be very long with a lot of objects...):
if clean: if clean:
print("Removing links...") print('Removing links...')
for lib in reversed(bpy.data.libraries): for lib in reversed(bpy.data.libraries):
bpy.data.libraries.remove(lib) bpy.data.libraries.remove(lib)
# Créer les dossiers intermediaires # Créer les dossiers intermediaires
build_path.parent.mkdir(parents=True, exist_ok=True) build_path.parent.mkdir(parents=True, exist_ok=True)
print("Saving to", build_path) print('Saving to', build_path)
bpy.ops.wm.save_as_mainfile(filepath=str(build_path), compress=False) bpy.ops.wm.save_as_mainfile(filepath=str(build_path), compress=False)
print("build time:", f"{time() - t0:.1f}s") print("build time:", f'{time() - t0:.1f}s')
bpy.ops.wm.quit_blender() bpy.ops.wm.quit_blender()
if __name__ == "__main__": if __name__ == '__main__' :
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(description='build_collection_blends',
description="build_collection_blends", formatter_class=argparse.ArgumentDefaultsHelpFormatter)
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
parser.add_argument( parser.add_argument('-path') # Trouve/créer le json assetlib.json en sous-dossier de libdir
"-path" parser.add_argument('--category') # Lit la category dans le json et a link tout dans le blend
) # Trouve/créer le json assetlib.json en sous-dossier de libdir
parser.add_argument(
"--category"
) # Lit la category dans le json et a link tout dans le blend
if "--" in sys.argv: if '--' in sys.argv :
index = sys.argv.index("--") index = sys.argv.index('--')
sys.argv = [sys.argv[index-1], *sys.argv[index+1:]] sys.argv = [sys.argv[index-1], *sys.argv[index+1:]]
args = parser.parse_args() args = parser.parse_args()
@@ -44,11 +44,10 @@ from asset_library.constants import ASSETLIB_FILENAME
] ]
""" """
def create_collection_json(path, source_directory): def create_collection_json(path, source_directory):
"""Create a Json from every marked collection in blends '''Create a Json from every marked collection in blends
contained in folderpath (respect hierachy) contained in folderpath (respect hierachy)
""" '''
json_path = Path(path) / ASSETLIB_FILENAME json_path = Path(path) / ASSETLIB_FILENAME
@@ -57,80 +56,75 @@ def create_collection_json(path, source_directory):
# or open all blends and look only for marked collection ? (if versionned, get still get only last) # or open all blends and look only for marked collection ? (if versionned, get still get only last)
# get all blend in dir and subdirs (only last when versionned _v???) # get all blend in dir and subdirs (only last when versionned _v???)
blends = get_last_files( blends = get_last_files(source_directory, pattern=r'(_v\d{3})?\.blend$', only_matching=True)
source_directory, pattern=r"(_v\d{3})?\.blend$", only_matching=True
)
root_path = Path(source_directory).as_posix().rstrip("/") + "/" root_path = Path(source_directory).as_posix().rstrip('/') + '/'
print("root_path: ", root_path) print('root_path: ', root_path)
# open and check data block marked as asset # open and check data block marked as asset
category_datas = [] category_datas = []
for i, blend in enumerate(blends): for i, blend in enumerate(blends):
fp = Path(blend) fp = Path(blend)
print(f"{i+1}/{len(blends)}") print(f'{i+1}/{len(blends)}')
## What is considered a grouping category ? top level folders ? parents[1] ? ## What is considered a grouping category ? top level folders ? parents[1] ?
## Remove root path and extension ## Remove root path and extension
## top level folder ('chars'), problem if blends at root ## top level folder ('chars'), problem if blends at root
category = fp.as_posix().replace(root_path, "").split("/")[0] category = fp.as_posix().replace(root_path, '').split('/')[0]
## full blend path (chars/perso/perso) ## full blend path (chars/perso/perso)
# category = fp.as_posix().replace(root_path, '').rsplit('.', 1)[0] # category = fp.as_posix().replace(root_path, '').rsplit('.', 1)[0]
print(category) print(category)
with bpy.data.libraries.load(blend, link=True, assets_only=True) as ( with bpy.data.libraries.load(blend, link=True, assets_only=True) as (data_from, data_to):
data_from,
data_to,
):
## just listing ## just listing
col_name_list = [c for c in data_from.collections] col_name_list = [c for c in data_from.collections]
if not col_name_list: if not col_name_list:
continue continue
col_list = next( col_list = next((c['children'] for c in category_datas if c['name'] == category), None)
(c["children"] for c in category_datas if c["name"] == category), None
)
if col_list is None: if col_list is None:
col_list = [] col_list = []
category_data = { category_data = {
"name": category, 'name': category,
"id": str(uuid.uuid4()), 'id': str(uuid.uuid4()),
"children": col_list, 'children': col_list,
} }
category_datas.append(category_data) category_datas.append(category_data)
blend_source_path = blend.as_posix() blend_source_path = blend.as_posix()
if project_root := os.environ.get("PROJECT_ROOT"): if (project_root := os.environ.get('PROJECT_ROOT')):
blend_source_path = blend_source_path.replace(project_root, "$PROJECT_ROOT") blend_source_path = blend_source_path.replace(project_root, '$PROJECT_ROOT')
for name in col_name_list: for name in col_name_list:
data = { data = {
"filepath": blend, 'filepath' : blend,
"name": name, 'name' : name,
# 'tags' : [], # 'tags' : [],
"metadata": {"filepath": blend_source_path}, 'metadata' : {'filepath': blend_source_path},
} }
col_list.append(data) col_list.append(data)
json_path.write_text(json.dumps(category_datas, indent="\t")) json_path.write_text(json.dumps(category_datas, indent='\t'))
## create text catalog from json (keep_existing_category ?) ## create text catalog from json (keep_existing_category ?)
create_catalog_file(json_path, keep_existing_category=True) create_catalog_file(json_path, keep_existing_category=True)
def create_collection_library(path, source_directory=None): def create_collection_library(path, source_directory=None):
""" '''
path: store collection library (json and blends database) path: store collection library (json and blends database)
source_directory: if a source is set, rebuild json and library source_directory: if a source is set, rebuild json and library
""" '''
if source_directory: if source_directory:
if not Path(source_directory).exists(): if not Path(source_directory).exists():
print(f"Source directory not exists: {source_directory}") print(f'Source directory not exists: {source_directory}')
return return
## scan source and build json in assetlib dir root ## scan source and build json in assetlib dir root
@@ -138,44 +132,31 @@ def create_collection_library(path, source_directory=None):
json_path = Path(path) / ASSETLIB_FILENAME json_path = Path(path) / ASSETLIB_FILENAME
if not json_path.exists(): if not json_path.exists():
print(f"No json found at: {json_path}") print(f'No json found at: {json_path}')
return return
file_datas = json.loads(json_path.read()) file_datas = json.loads(json_path.read())
## For each category in json, execute build_assets_blend script ## For each category in json, execute build_assets_blend script
script = Path(__file__).parent / "build_collection_blends.py" script = Path(__file__).parent / 'build_collection_blends.py'
#empty_blend = Path(__file__).parent / 'empty_scene.blend' #empty_blend = Path(__file__).parent / 'empty_scene.blend'
# for category, asset_datas in file_datas.items(): # for category, asset_datas in file_datas.items():
for category_data in file_datas: for category_data in file_datas:
## add an empty blend as second arg ## add an empty blend as second arg
cmd = [ cmd = [bpy.app.binary_path, '--python', str(script), '--', '--path', path, '--category', category_data['name']]
bpy.app.binary_path, print('cmd: ', cmd)
"--python",
str(script),
"--",
"--path",
path,
"--category",
category_data["name"],
]
print("cmd: ", cmd)
subprocess.call(cmd) subprocess.call(cmd)
if __name__ == "__main__": if __name__ == '__main__' :
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(description='Create Collection Library',
description="Create Collection Library", formatter_class=argparse.ArgumentDefaultsHelpFormatter)
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
parser.add_argument( parser.add_argument('--path') # trouve/créer le json assetlib.json en sous-dossier de libdir
"--path"
) # trouve/créer le json assetlib.json en sous-dossier de libdir
if "--" in sys.argv: if '--' in sys.argv :
index = sys.argv.index("--") index = sys.argv.index('--')
sys.argv = [sys.argv[index-1], *sys.argv[index+1:]] sys.argv = [sys.argv[index-1], *sys.argv[index+1:]]
args = parser.parse_args() args = parser.parse_args()
@@ -1,3 +1,4 @@
import bpy import bpy
@@ -8,6 +9,6 @@ def draw_context_menu(layout):
def draw_header(layout): def draw_header(layout):
"""Draw the header of the Asset Browser Window""" '''Draw the header of the Asset Browser Window'''
return return
@@ -1,10 +1,11 @@
from typing import List, Tuple from typing import List, Tuple
import bpy import bpy
addon_keymaps: List[Tuple[bpy.types.KeyMap, bpy.types.KeyMapItem]] = [] addon_keymaps: List[Tuple[bpy.types.KeyMap, bpy.types.KeyMapItem]] = []
def register(): def register():
wm = bpy.context.window_manager wm = bpy.context.window_manager
addon = wm.keyconfigs.addon addon = wm.keyconfigs.addon
@@ -12,12 +13,9 @@ def register():
return return
km = addon.keymaps.new(name="File Browser Main", space_type="FILE_BROWSER") km = addon.keymaps.new(name="File Browser Main", space_type="FILE_BROWSER")
kmi = km.keymap_items.new( kmi = km.keymap_items.new("assetlib.load_asset", "LEFTMOUSE", "DOUBLE_CLICK") # , shift=True
"assetlib.load_asset", "LEFTMOUSE", "DOUBLE_CLICK"
) # , shift=True
addon_keymaps.append((km, kmi)) addon_keymaps.append((km, kmi))
def unregister(): def unregister():
for km, kmi in addon_keymaps: for km, kmi in addon_keymaps:
km.keymap_items.remove(kmi) km.keymap_items.remove(kmi)
@@ -14,11 +14,12 @@ from asset_library.common.bl_utils import load_col
from asset_library.common.functions import get_active_library from asset_library.common.functions import get_active_library
class ASSETLIB_OT_load_asset(Operator): class ASSETLIB_OT_load_asset(Operator):
bl_idname = "assetlib.load_asset" bl_idname = "assetlib.load_asset"
bl_options = {"REGISTER", "UNDO", "INTERNAL"} bl_options = {"REGISTER", "UNDO", "INTERNAL"}
bl_label = "Load Asset" bl_label = 'Load Asset'
bl_description = "Link and override asset in current file" bl_description = 'Link and override asset in current file'
@classmethod @classmethod
def poll(cls, context: Context) -> bool: def poll(cls, context: Context) -> bool:
@@ -27,10 +28,10 @@ class ASSETLIB_OT_load_asset(Operator):
return False return False
lib = get_active_library() lib = get_active_library()
if not lib or lib.data_type != "COLLECTION": if not lib or lib.data_type != 'COLLECTION':
return False return False
if not context.active_file or "filepath" not in context.active_file.asset_data: if not context.active_file or 'filepath' not in context.active_file.asset_data:
cls.poll_message_set("Has not filepath property") cls.poll_message_set("Has not filepath property")
return False return False
@@ -38,49 +39,51 @@ class ASSETLIB_OT_load_asset(Operator):
def execute(self, context: Context) -> Set[str]: def execute(self, context: Context) -> Set[str]:
print("Load Asset") print('Load Asset')
lib = get_active_library() lib = get_active_library()
asset = context.active_file asset = context.active_file
if not asset: if not asset:
self.report({"ERROR"}, "No asset selected") self.report({"ERROR"}, 'No asset selected')
return {"CANCELLED"} return {'CANCELLED'}
active_lib = lib.library_type.get_active_asset_library() active_lib = lib.library_type.get_active_asset_library()
asset_path = asset.asset_data["filepath"] asset_path = asset.asset_data['filepath']
asset_path = active_lib.library_type.format_path(asset_path) asset_path = active_lib.library_type.format_path(asset_path)
name = asset.name name = asset.name
## set mode to object ## set mode to object
if context.mode != "OBJECT": if context.mode != 'OBJECT':
bpy.ops.object.mode_set(mode="OBJECT") bpy.ops.object.mode_set(mode='OBJECT')
if not Path(asset_path).exists(): if not Path(asset_path).exists():
self.report({"ERROR"}, f"Not exists: {asset_path}") self.report({'ERROR'}, f'Not exists: {asset_path}')
return {"CANCELLED"} return {'CANCELLED'}
print("Load collection", asset_path, name) print('Load collection', asset_path, name)
res = load_col(asset_path, name, link=True, override=True, rig_pattern="*_rig") res = load_col(asset_path, name, link=True, override=True, rig_pattern='*_rig')
if res: if res:
if res.type == "ARMATURE": if res.type == 'ARMATURE':
self.report({"INFO"}, f"Override rig {res.name}") self.report({'INFO'}, f'Override rig {res.name}')
elif res.type == "EMPTY": elif res.type == 'EMPTY':
self.report({"INFO"}, f"Instance collection {res.name}") self.report({'INFO'}, f'Instance collection {res.name}')
return {"FINISHED"} return {'FINISHED'}
### --- REGISTER --- ### --- REGISTER ---
classes = (ASSETLIB_OT_load_asset,) classes = (
ASSETLIB_OT_load_asset,
)
def register(): 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)
@@ -1,6 +1,8 @@
from asset_library.file import operators, gui, keymaps
if "bpy" in locals(): from asset_library.data_type.file import (
operators, gui, keymaps)
if 'bpy' in locals():
import importlib import importlib
importlib.reload(operators) importlib.reload(operators)
@@ -9,12 +11,10 @@ if "bpy" in locals():
import bpy import bpy
def register(): def register():
operators.register() operators.register()
keymaps.register() keymaps.register()
def unregister(): def unregister():
operators.unregister() operators.unregister()
keymaps.unregister() keymaps.unregister()
+170
View File
@@ -0,0 +1,170 @@
import argparse
import sys
import json
from pathlib import Path
import bpy
import re
import uuid
from itertools import groupby
from asset_library.constants import ASSETLIB_FILENAME, MODULE_DIR
from asset_library.common.bl_utils import thumbnail_blend_file
from asset_library.common.functions import command
@command
def bundle_library(source_directory, bundle_directory, template_info, thumbnail_template,
template=None, data_file=None):
field_pattern = r'{(\w+)}'
asset_data_path = Path(bundle_directory) / ASSETLIB_FILENAME
glob_pattern = re.sub(field_pattern, '*', template)
re_pattern = re.sub(field_pattern, r'([\\w -_.]+)', template)
re_pattern = re_pattern.replace('?', '.')
field_names = re.findall(field_pattern, template)
asset_file_datas = []
for f in sorted(Path(source_directory).glob(glob_pattern)):
rel_path = f.relative_to(source_directory).as_posix()
field_values = re.findall(re_pattern, rel_path)[0]
field_data = {k:v for k,v in zip(field_names, field_values)}
name = field_data.get('name', f.stem)
thumbnail = (f / thumbnail_template.format(name=name)).resolve()
asset_data = (f / template_info.format(name=name)).resolve()
catalogs = sorted([v for k,v in sorted(field_data.items()) if re.findall('cat[0-9]+', k)])
catalogs = [c.replace('_', ' ').title() for c in catalogs]
if not thumbnail.exists():
thumbnail_blend_file(f, thumbnail)
asset_data = {
'catalog' : '/'.join(catalogs),
'preview' : thumbnail.as_posix(), #'./' + bpy.path.relpath(str(thumbnail), start=str(f))[2:],
'filepath' : f.as_posix(), #'./' + bpy.path.relpath(str(f), start=str(asset_data_path))[2:],
'name': name,
'tags': [],
'metadata': {'filepath': f.as_posix()}
}
asset_file_datas.append(asset_data)
# Write json data file to store all asset found
print(f'Writing asset data file to, {asset_data_path}')
asset_data_path.write_text(json.dumps(asset_file_datas, indent=4))
#script = MODULE_DIR / 'common' / 'bundle_blend.py'
#cmd = [bpy.app.binary_path, '--python', str(script), '--', '--filepath', str(filepath)]
#print(cmd)
#subprocess.call(cmd)
@command
def bundle_blend(filepath, depth=0):
#print('Bundle Blend...')
filepath = Path(filepath)
#asset_data_path = get_asset_datas_file(filepath)
asset_data_path = filepath / ASSETLIB_FILENAME
blend_name = filepath.name.replace(' ', '_').lower()
blend_path = (filepath / blend_name).with_suffix('.blend')
if not asset_data_path.exists():
raise Exception(f'The file {asset_data_path} not exist')
catalog_path = get_catalog_path(filepath)
catalog_data = read_catalog(catalog_path)
asset_file_data = json.loads(asset_data_path.read_text())
#asset_file_data = {i['catalog']:i for i in asset_file_data}
if depth == 0:
groups = [asset_file_data]
else:
asset_file_data.sort(key=lambda x :x['catalog'].split('/')[:depth])
groups = groupby(asset_file_data, key=lambda x :x['catalog'].split('/')[:depth])
#progress = 0
total_assets = len(asset_file_data)
i = 0
for sub_path, asset_datas in groups:
bpy.ops.wm.read_homefile(use_empty=True)
for asset_data in asset_datas:
blend_name = sub_path[-1].replace(' ', '_').lower()
blend_path = Path(filepath, *sub_path, blend_name).with_suffix('.blend')
if i % int(total_assets / 100) == 0:
print(f'Progress: {int(i / total_assets * 100)}')
col = bpy.data.collections.new(name=asset_data['name'])
# Seems slow
#bpy.context.scene.collection.children.link(col)
col.asset_mark()
with bpy.context.temp_override(id=col):
bpy.ops.ed.lib_id_load_custom_preview(
filepath=asset_data['preview']
)
col.asset_data.description = asset_data.get('description', '')
catalog_name = asset_data['catalog']
catalog = catalog_data.get(catalog_name)
if not catalog:
catalog = {'id': str(uuid.uuid4()), 'name': catalog_name}
catalog_data[catalog_name] = catalog
col.asset_data.catalog_id = catalog['id']
for k, v in asset_data.get('metadata', {}).items():
col.asset_data[k] = v
i += 1
print(f'Saving Blend to {blend_path}')
blend_path.mkdir(exist_ok=True, parents=True)
bpy.ops.wm.save_as_mainfile(filepath=str(blend_path), compress=True)
write_catalog(catalog_path, catalog_data)
bpy.ops.wm.quit_blender()
if __name__ == '__main__' :
parser = argparse.ArgumentParser(description='bundle_blend',
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument('--source-path')
parser.add_argument('--bundle-path')
parser.add_argument('--asset-data-template')
parser.add_argument('--thumbnail-template')
parser.add_argument('--template', default=None)
parser.add_argument('--data-file', default=None)
parser.add_argument('--depth', default=0, type=int)
if '--' in sys.argv :
index = sys.argv.index('--')
sys.argv = [sys.argv[index-1], *sys.argv[index+1:]]
args = parser.parse_args()
bundle_library(
source_directory=args.source_directory,
bundle_directory=args.bundle_directory,
template_info=args.template_info,
thumbnail_template=args.thumbnail_template,
template=args.template,
data_file=args.data_file)
bundle_blend(filepath=args.bundle_directory, depth=args.depth)
+3 -4
View File
@@ -1,3 +1,4 @@
import bpy import bpy
from pathlib import Path from pathlib import Path
@@ -22,9 +23,7 @@ def draw_context_menu(layout):
lib = get_active_library() lib = get_active_library()
filepath = lib.library_type.get_active_asset_path() filepath = lib.library_type.get_active_asset_path()
layout.operator( layout.operator("assetlib.open_blend_file", text="Open Blend File")#.filepath = asset.asset_data['filepath']
"assetlib.open_blend_file", text="Open Blend File"
) # .filepath = asset.asset_data['filepath']
op = layout.operator("wm.link", text="Link") op = layout.operator("wm.link", text="Link")
op.filepath = str(filepath) op.filepath = str(filepath)
@@ -33,7 +32,7 @@ def draw_context_menu(layout):
def draw_header(layout): def draw_header(layout):
"""Draw the header of the Asset Browser Window""" '''Draw the header of the Asset Browser Window'''
layout.separator() layout.separator()
#layout.operator("actionlib.store_anim_pose", text='Add Action', icon='FILE_NEW') #layout.operator("actionlib.store_anim_pose", text='Add Action', icon='FILE_NEW')
@@ -1,3 +1,5 @@
from typing import List, Tuple from typing import List, Tuple
import bpy import bpy
@@ -5,16 +7,13 @@ from bpy.app.handlers import persistent
addon_keymaps: List[Tuple[bpy.types.KeyMap, bpy.types.KeyMapItem]] = [] addon_keymaps: List[Tuple[bpy.types.KeyMap, bpy.types.KeyMapItem]] = []
def register() -> None: def register() -> None:
wm = bpy.context.window_manager wm = bpy.context.window_manager
if not wm.keyconfigs.addon: if not wm.keyconfigs.addon:
# This happens when Blender is running in the background. # This happens when Blender is running in the background.
return return
km = wm.keyconfigs.addon.keymaps.new( km = wm.keyconfigs.addon.keymaps.new(name="File Browser Main", space_type="FILE_BROWSER")
name="File Browser Main", space_type="FILE_BROWSER"
)
kmi = km.keymap_items.new("assetlib.open_blend_file", "LEFTMOUSE", "DOUBLE_CLICK") kmi = km.keymap_items.new("assetlib.open_blend_file", "LEFTMOUSE", "DOUBLE_CLICK")
addon_keymaps.append((km, kmi)) addon_keymaps.append((km, kmi))
@@ -1,14 +1,12 @@
import bpy import bpy
from bpy.types import Context, Operator from bpy.types import Context, Operator
from bpy_extras import asset_utils from bpy_extras import asset_utils
from bpy.props import StringProperty from bpy.props import StringProperty
from typing import List, Tuple, Set from typing import List, Tuple, Set
from asset_library.common.file_utils import ( from asset_library.common.file_utils import (open_blender_file,
open_blender_file, synchronize, open_blender_file)
synchronize,
open_blender_file,
)
from asset_library.common.functions import get_active_library from asset_library.common.functions import get_active_library
@@ -16,8 +14,8 @@ from asset_library.common.functions import get_active_library
class ASSETLIB_OT_open_blend_file(Operator): class ASSETLIB_OT_open_blend_file(Operator):
bl_idname = "assetlib.open_blend_file" bl_idname = "assetlib.open_blend_file"
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
bl_label = "Open Blender File" bl_label = 'Open Blender File'
bl_description = "Open blender file" bl_description = 'Open blender file'
@classmethod @classmethod
def poll(cls, context: Context) -> bool: def poll(cls, context: Context) -> bool:
@@ -26,10 +24,10 @@ class ASSETLIB_OT_open_blend_file(Operator):
return False return False
lib = get_active_library() lib = get_active_library()
if not lib or lib.data_type != "FILE": if not lib or lib.data_type != 'FILE':
return False return False
if not context.active_file or "filepath" not in context.active_file.asset_data: if not context.active_file or 'filepath' not in context.active_file.asset_data:
cls.poll_message_set("Has not filepath property") cls.poll_message_set("Has not filepath property")
return False return False
@@ -43,17 +41,17 @@ class ASSETLIB_OT_open_blend_file(Operator):
open_blender_file(filepath) open_blender_file(filepath)
return {"FINISHED"} return {'FINISHED'}
classes = (ASSETLIB_OT_open_blend_file,) classes = (
ASSETLIB_OT_open_blend_file,
)
def register(): 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)
+107
View File
@@ -0,0 +1,107 @@
from pathlib import Path
from datetime import datetime
import subprocess
from ...core.bl_utils import get_bl_cmd
from ...core.lib_utils import get_asset_data
from ...core.catalog import read_catalog
from ... import constants
import bpy
from bpy.types import Operator
from bpy.props import StringProperty, EnumProperty
class ASSETLIB_OT_update_materials(Operator):
bl_idname = 'assetlibrary.update_materials'
bl_label = 'Update node'
bl_options = {"REGISTER", "UNDO"}
selection : EnumProperty(items=[(s, s.title(), '') for s in ('ALL', 'OBJECT', 'CURRENT')], default="CURRENT")
@classmethod
def poll(cls, context):
return context.object and context.object.active_material
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self)
def execute(self, context):
asset_libraries = context.preferences.filepaths.asset_libraries
ob = bpy.context.object
ntree = context.space_data.edit_tree
ntree_name = ntree.name
new_ntree = None
if self.selection == 'OBJECT':
materials = [s.material for s in ob.material_slots if s.material]
elif self.selection == 'CURRENT':
materials = [ob.active_material]
else:
materials = list(bpy.data.materials)
mat_names = set(m.name for m in materials)
for asset_library in asset_libraries:
library_path = Path(asset_library.path)
blend_files = [fp for fp in library_path.glob("**/*.blend") if fp.is_file()]
images = list(bpy.data.images)
materials = list(bpy.data.materials)# Storing original materials to compare with imported ones
link = (asset_library.import_method == 'LINK')
for blend_file in blend_files:
print(blend_file)
with bpy.data.libraries.load(str(blend_file), assets_only=True, link=link) as (data_from, data_to):
import_materials = [n for n in data_from.materials if n in mat_names]
print("import_materials", import_materials)
data_to.materials = import_materials
mat_names -= set(import_materials) # Store already updated nodes
new_materials = set(m for m in bpy.data.materials if m not in materials)
new_images = set(i for i in bpy.data.images if i not in images)
#
for new_mat in new_materials:
new_mat_name = new_mat.library_weak_reference.id_name[2:]
local_mat = next((m for m in bpy.data.materials if m.name == new_mat_name and m != new_mat), None)
if not local_mat:
print(f'No local material {new_mat_name}')
continue
print(f'Merge material {local_mat.name} into {new_mat.name}')
local_mat.user_remap(new_mat)
bpy.data.materials.remove(local_mat)
if not new_mat.library:
new_mat.name = new_mat_name
new_mat.asset_clear()
return {'FINISHED'}
def draw(self, context):
layout = self.layout
layout.prop(self, "selection", expand=True)
bl_classes = (
ASSETLIB_OT_update_materials,
)
def register():
for bl_class in bl_classes:
bpy.utils.register_class(bl_class)
def unregister():
for bl_class in reversed(bl_classes):
bpy.utils.unregister_class(bl_class)
+31
View File
@@ -0,0 +1,31 @@
"""
This module contains blender UI elements in the node editor
:author: Autour de Minuit
:maintainers: Christophe Seux
:date: 2024
"""
import bpy
def draw_menu(self, context):
layout = self.layout
row = layout.row(align=False)
layout.operator('assetlibrary.update_materials', text='Update Materials', icon='MATERIAL')
def register():
# for c in classes:
# bpy.utils.register_class(c)
bpy.types.ASSETLIB_MT_node_editor.append(draw_menu)
def unregister():
# for c in reversed(classes):
# bpy.utils.unregister_class(c)
bpy.types.ASSETLIB_MT_node_editor.remove(draw_menu)
+138
View File
@@ -0,0 +1,138 @@
from pathlib import Path
from datetime import datetime
import subprocess
from ...core.bl_utils import get_bl_cmd
from ...core.lib_utils import get_asset_data, asset_library_map
from ...core.catalog import read_catalog
from ... import constants
import bpy
from bpy.types import Operator
from bpy.props import StringProperty, EnumProperty
class ASSETLIB_OT_update_nodes(Operator):
bl_idname = 'assetlibrary.update_nodes'
bl_label = 'Update node'
bl_options = {"REGISTER", "UNDO"}
selection : EnumProperty(items=[(s, s.title(), '') for s in ('ALL', 'SELECTED', 'CURRENT')], default="CURRENT", name='All Nodes')
@classmethod
def poll(cls, context):
return context.space_data.edit_tree
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self)
def execute(self, context):
asset_libraries = context.preferences.filepaths.asset_libraries
ntree = context.space_data.edit_tree
ntree_name = ntree.name
new_ntree = None
if self.selection == 'SELECTED':
nodes = [ n.node_tree for n in context.space_data.edit_tree.nodes
if n.type == "GROUP" and n.select]
elif self.selection == 'CURRENT':
active_node = context.space_data.edit_tree
nodes = [active_node]
else:
nodes = list(bpy.data.node_groups)
library_map = asset_library_map()
#node_groups = list(bpy.data.node_groups)
#images = list(bpy.data.images)
#materials = list(bpy.data.materials)
with remap_datablock_duplicates():
for node in nodes:
blend_file = find_asset_source(library_map, 'node_groups', node.name)
link = bool(node.library)
with bpy.data.libraries.load(str(blend_file), link=link) as (data_from, data_to):
data_to.node_groups = [node.name]
for new_node_group in new_node_groups:
new_node_group_name = new_node_group.library_weak_reference.id_name[2:]
local_node_group = next((n for n in bpy.data.node_groups if n.name == new_node_group_name and n != new_node_group), None)
if not local_node_group:
print(f'No local node_group {new_node_group_name}')
continue
print(f'Merge node {local_node_group.name} into {new_node_group.name}')
local_node_group.user_remap(new_node_group)
new_node_group.interface_update(context)
bpy.data.node_groups.remove(local_node_group)
new_node_group.name = new_node_group_name
new_node_group.asset_clear()
node_names = set(n.name for n in nodes)
for asset_library in asset_libraries:
library_path = Path(asset_library.path)
blend_files = [fp for fp in library_path.glob("**/*.blend") if fp.is_file()]
node_groups = list(bpy.data.node_groups)# Storing original node_geoup to compare with imported
link = (asset_library.import_method == 'LINK')
for blend_file in blend_files:
print(blend_file)
with bpy.data.libraries.load(str(blend_file), assets_only=True, link=link) as (data_from, data_to):
import_node_groups = [n for n in data_from.node_groups if n in node_names]
print("import_node_groups", import_node_groups)
data_to.node_groups = import_node_groups
node_names -= set(import_node_groups) # Store already updated nodes
new_node_groups = set(n for n in bpy.data.node_groups if n not in node_groups)
for new_node_group in new_node_groups:
new_node_group_name = new_node_group.library_weak_reference.id_name[2:]
local_node_group = next((n for n in bpy.data.node_groups if n.name == new_node_group_name and n != new_node_group), None)
if not local_node_group:
print(f'No local node_group {new_node_group_name}')
continue
print(f'Merge node {local_node_group.name} into {new_node_group.name}')
local_node_group.user_remap(new_node_group)
new_node_group.interface_update(context)
bpy.data.node_groups.remove(local_node_group)
new_node_group.name = new_node_group_name
new_node_group.asset_clear()
return {'FINISHED'}
def draw(self, context):
layout = self.layout
layout.prop(self, "selection", expand=True)
bl_classes = (
ASSETLIB_OT_update_nodes,
)
def register():
for bl_class in bl_classes:
bpy.utils.register_class(bl_class)
def unregister():
for bl_class in reversed(bl_classes):
bpy.utils.unregister_class(bl_class)
+31
View File
@@ -0,0 +1,31 @@
"""
This module contains blender UI elements in the node editor
:author: Autour de Minuit
:maintainers: Christophe Seux
:date: 2024
"""
import bpy
def draw_menu(self, context):
layout = self.layout
row = layout.row(align=False)
layout.operator('assetlibrary.update_nodes', text='Update Node Group', icon='IMPORT')
def register():
# for c in classes:
# bpy.utils.register_class(c)
bpy.types.ASSETLIB_MT_node_editor.append(draw_menu)
def unregister():
# for c in reversed(classes):
# bpy.utils.unregister_class(c)
bpy.types.ASSETLIB_MT_node_editor.remove(draw_menu)
@@ -1,14 +1,14 @@
from asset_library.pose import operators
if "bpy" in locals(): from asset_library.pose import (
operators)
if 'bpy' in locals():
import importlib import importlib
importlib.reload(operators) importlib.reload(operators)
def register(): def register():
operators.register() operators.register()
def unregister(): def unregister():
operators.unregister() operators.unregister()
@@ -39,7 +39,7 @@ def convert_old_poselib(old_poselib: Action) -> Collection[Action]:
# appropriate frame in the scene (to set up things like the background # appropriate frame in the scene (to set up things like the background
# colour), but the old-style poselib doesn't contain such information. All # colour), but the old-style poselib doesn't contain such information. All
# we can do is just render on the current frame. # we can do is just render on the current frame.
bpy.ops.asset.mark({"selected_ids": pose_assets}) bpy.ops.asset.mark({'selected_ids': pose_assets})
return pose_assets return pose_assets
+76 -133
View File
@@ -22,13 +22,7 @@ import subprocess
import uuid import uuid
import time import time
from bpy.props import ( from bpy.props import BoolProperty, CollectionProperty, EnumProperty, PointerProperty, StringProperty
BoolProperty,
CollectionProperty,
EnumProperty,
PointerProperty,
StringProperty,
)
from bpy.types import ( from bpy.types import (
Action, Action,
Context, Context,
@@ -41,12 +35,16 @@ from bpy.types import (
from bpy_extras import asset_utils from bpy_extras import asset_utils
from bpy_extras.io_utils import ExportHelper, ImportHelper from bpy_extras.io_utils import ExportHelper, ImportHelper
from asset_library.action.functions import ( from asset_library.data_type.action.functions import (
get_marker, get_marker,
get_keyframes, get_keyframes,
) )
from asset_library.common.bl_utils import get_view3d_persp, load_assets_from, split_path from asset_library.common.bl_utils import (
get_view3d_persp,
load_assets_from,
split_path
)
class POSELIB_OT_create_pose_asset(Operator): class POSELIB_OT_create_pose_asset(Operator):
@@ -61,22 +59,19 @@ class POSELIB_OT_create_pose_asset(Operator):
pose_name: StringProperty(name="Pose Name") # type: ignore pose_name: StringProperty(name="Pose Name") # type: ignore
activate_new_action: BoolProperty(name="Activate New Action", default=True) # type: ignore activate_new_action: BoolProperty(name="Activate New Action", default=True) # type: ignore
@classmethod @classmethod
def poll(cls, context: Context) -> bool: def poll(cls, context: Context) -> bool:
# Make sure that if there is an asset browser open, the artist can see the newly created pose asset. # Make sure that if there is an asset browser open, the artist can see the newly created pose asset.
asset_browse_area: Optional[bpy.types.Area] = asset_browser.area_from_context( asset_browse_area: Optional[bpy.types.Area] = asset_browser.area_from_context(context)
context
)
if not asset_browse_area: if not asset_browse_area:
# No asset browser is visible, so there also aren't any expectations # No asset browser is visible, so there also aren't any expectations
# that this asset will be visible. # that this asset will be visible.
return True return True
asset_space_params = asset_browser.params(asset_browse_area) asset_space_params = asset_browser.params(asset_browse_area)
if asset_space_params.asset_library_ref != "LOCAL": if asset_space_params.asset_library_ref != 'LOCAL':
cls.poll_message_set( cls.poll_message_set("Asset Browser must be set to the Current File library")
"Asset Browser must be set to the Current File library"
)
return False return False
return True return True
@@ -90,28 +85,28 @@ class POSELIB_OT_create_pose_asset(Operator):
if pose_name: if pose_name:
prefix = True prefix = True
asset_name = Path(bpy.data.filepath).stem.split("_")[0] asset_name = Path(bpy.data.filepath).stem.split('_')[0]
action_asset_name = re.search(f"^{asset_name}.", pose_name) action_asset_name = re.search(f'^{asset_name}.', pose_name)
if action_asset_name: if action_asset_name:
pose_name = pose_name.replace(action_asset_name.group(0), "") pose_name = pose_name.replace(action_asset_name.group(0), '')
side = re.search("_\w$", pose_name) side = re.search('_\w$', pose_name)
if side: if side:
pose_name = pose_name.replace(side.group(0), "") pose_name = pose_name.replace(side.group(0), '')
if "hands" in context.object.animation_data.action.name.lower(): if 'hands' in context.object.animation_data.action.name.lower():
pose_name = f"hand_{pose_name}" pose_name = f'hand_{pose_name}'
if pose_name.startswith("lips_"): if pose_name.startswith('lips_'):
pose_name.replace("lips_", "") pose_name.replace('lips_', '')
split = pose_name.split("_") split = pose_name.split('_')
pose_name = "-".join([s for s in split if s.isupper()]) pose_name = '-'.join([s for s in split if s.isupper()])
pose_name = f"{pose_name}_{split[-1]}" pose_name = f'{pose_name}_{split[-1]}'
prefix = False prefix = False
if prefix and not pose_name.startswith(asset_name): if prefix and not pose_name.startswith(asset_name):
pose_name = f"{asset_name}_{pose_name}" pose_name = f'{asset_name}_{pose_name}'
else: else:
pose_name = self.pose_name or context.object.name pose_name = self.pose_name or context.object.name
@@ -131,6 +126,7 @@ class POSELIB_OT_create_pose_asset(Operator):
if context.scene.camera: if context.scene.camera:
data_dict.update(dict(camera=context.scene.camera.name)) data_dict.update(dict(camera=context.scene.camera.name))
for k, v in data_dict.items(): for k, v in data_dict.items():
data[k] = v data[k] = v
### ###
@@ -138,7 +134,7 @@ class POSELIB_OT_create_pose_asset(Operator):
if self.activate_new_action: if self.activate_new_action:
self._set_active_action(context, asset) self._set_active_action(context, asset)
self._activate_asset_in_browser(context, asset) self._activate_asset_in_browser(context, asset)
return {"FINISHED"} return {'FINISHED'}
def _set_active_action(self, context: Context, asset: Action) -> None: def _set_active_action(self, context: Context, asset: Action) -> None:
self._prevent_action_loss(context.object) self._prevent_action_loss(context.object)
@@ -153,9 +149,7 @@ class POSELIB_OT_create_pose_asset(Operator):
This makes it possible to immediately check & edit the created pose asset. This makes it possible to immediately check & edit the created pose asset.
""" """
asset_browse_area: Optional[bpy.types.Area] = asset_browser.area_from_context( asset_browse_area: Optional[bpy.types.Area] = asset_browser.area_from_context(context)
context
)
if not asset_browse_area: if not asset_browse_area:
return return
@@ -187,9 +181,7 @@ class POSELIB_OT_create_pose_asset(Operator):
return return
action.use_fake_user = True action.use_fake_user = True
self.report( self.report({'WARNING'}, "Action %s marked Fake User to prevent loss" % action.name)
{"WARNING"}, "Action %s marked Fake User to prevent loss" % action.name
)
class POSELIB_OT_restore_previous_action(Operator): class POSELIB_OT_restore_previous_action(Operator):
@@ -223,17 +215,17 @@ class POSELIB_OT_restore_previous_action(Operator):
self._timer = wm.event_timer_add(0.001, window=context.window) self._timer = wm.event_timer_add(0.001, window=context.window)
wm.modal_handler_add(self) wm.modal_handler_add(self)
return {"RUNNING_MODAL"} return {'RUNNING_MODAL'}
def modal(self, context, event): def modal(self, context, event):
if event.type != "TIMER": if event.type != 'TIMER':
return {"RUNNING_MODAL"} return {'RUNNING_MODAL'}
wm = context.window_manager wm = context.window_manager
wm.event_timer_remove(self._timer) wm.event_timer_remove(self._timer)
context.object.pose.apply_pose_from_action(self.pose_action) context.object.pose.apply_pose_from_action(self.pose_action)
return {"FINISHED"} return {'FINISHED'}
class ASSET_OT_assign_action(Operator): class ASSET_OT_assign_action(Operator):
@@ -265,9 +257,7 @@ class ASSET_OT_assign_action(Operator):
class POSELIB_OT_copy_as_asset(Operator): class POSELIB_OT_copy_as_asset(Operator):
bl_idname = "poselib.copy_as_asset" bl_idname = "poselib.copy_as_asset"
bl_label = "Copy Pose As Asset" bl_label = "Copy Pose As Asset"
bl_description = ( bl_description = "Create a new pose asset on the clipboard, to be pasted into an Asset Browser"
"Create a new pose asset on the clipboard, to be pasted into an Asset Browser"
)
bl_options = {"REGISTER"} bl_options = {"REGISTER"}
CLIPBOARD_ASSET_MARKER = "ASSET-BLEND=" CLIPBOARD_ASSET_MARKER = "ASSET-BLEND="
@@ -299,10 +289,7 @@ class POSELIB_OT_copy_as_asset(Operator):
filepath, filepath,
) )
asset_browser.tag_redraw(context.screen) asset_browser.tag_redraw(context.screen)
self.report( self.report({"INFO"}, "Pose Asset copied, use Paste As New Asset in any Asset Browser to paste")
{"INFO"},
"Pose Asset copied, use Paste As New Asset in any Asset Browser to paste",
)
# The asset has been saved to disk, so to clean up it has to loose its asset & fake user status. # The asset has been saved to disk, so to clean up it has to loose its asset & fake user status.
asset.asset_clear() asset.asset_clear()
@@ -313,10 +300,7 @@ class POSELIB_OT_copy_as_asset(Operator):
if asset.users > 0: if asset.users > 0:
# This should never happen, and indicates a bug in the code. Having a warning about it is nice, # This should never happen, and indicates a bug in the code. Having a warning about it is nice,
# but it shouldn't stand in the way of actually cleaning up the meant-to-be-temporary datablock. # but it shouldn't stand in the way of actually cleaning up the meant-to-be-temporary datablock.
self.report( self.report({"WARNING"}, "Unexpected non-zero user count for the asset, please report this as a bug")
{"WARNING"},
"Unexpected non-zero user count for the asset, please report this as a bug",
)
bpy.data.actions.remove(asset) bpy.data.actions.remove(asset)
return {"FINISHED"} return {"FINISHED"}
@@ -347,10 +331,8 @@ class POSELIB_OT_paste_asset(Operator):
return False return False
asset_lib_ref = context.space_data.params.asset_library_ref asset_lib_ref = context.space_data.params.asset_library_ref
if asset_lib_ref != "LOCAL": if asset_lib_ref != 'LOCAL':
cls.poll_message_set( cls.poll_message_set("Asset Browser must be set to the Current File library")
"Asset Browser must be set to the Current File library"
)
return False return False
# Delay checking the clipboard as much as possible, as it's CPU-heavier than the other checks. # Delay checking the clipboard as much as possible, as it's CPU-heavier than the other checks.
@@ -366,6 +348,7 @@ class POSELIB_OT_paste_asset(Operator):
return True return True
def execute(self, context: Context) -> Set[str]: def execute(self, context: Context) -> Set[str]:
clipboard = context.window_manager.clipboard clipboard = context.window_manager.clipboard
marker_len = len(POSELIB_OT_copy_as_asset.CLIPBOARD_ASSET_MARKER) marker_len = len(POSELIB_OT_copy_as_asset.CLIPBOARD_ASSET_MARKER)
@@ -402,12 +385,12 @@ class POSELIB_OT_pose_asset_select_bones(Operator):
#bl_property = "selected_side" #bl_property = "selected_side"
selected_side: EnumProperty( selected_side: EnumProperty(
name="Selected Side", name='Selected Side',
items=( items=(
("CURRENT", "Current", ""), ('CURRENT', "Current", ""),
("FLIPPED", "Flipped", ""), ('FLIPPED', "Flipped", ""),
("BOTH", "Both", ""), ('BOTH', "Both", ""),
), )
) )
@classmethod @classmethod
@@ -419,7 +402,7 @@ class POSELIB_OT_pose_asset_select_bones(Operator):
and context.asset_file_handle and context.asset_file_handle
): ):
return False return False
return context.asset_file_handle.id_type == "ACTION" return context.asset_file_handle.id_type == 'ACTION'
def execute(self, context: Context) -> Set[str]: def execute(self, context: Context) -> Set[str]:
asset: FileSelectEntry = context.asset_file_handle asset: FileSelectEntry = context.asset_file_handle
@@ -434,9 +417,7 @@ class POSELIB_OT_pose_asset_select_bones(Operator):
def _load_and_use_pose(self, context: Context) -> Set[str]: def _load_and_use_pose(self, context: Context) -> Set[str]:
asset_library_ref = context.asset_library_ref asset_library_ref = context.asset_library_ref
asset = context.asset_file_handle asset = context.asset_file_handle
asset_lib_path = bpy.types.AssetHandle.get_full_library_path( asset_lib_path = bpy.types.AssetHandle.get_full_library_path(asset, asset_library_ref)
asset, asset_library_ref
)
if not asset_lib_path: if not asset_lib_path:
self.report( # type: ignore self.report( # type: ignore
@@ -445,7 +426,7 @@ class POSELIB_OT_pose_asset_select_bones(Operator):
f"Selected asset {asset.name} could not be located inside the asset library", f"Selected asset {asset.name} could not be located inside the asset library",
) )
return {"CANCELLED"} return {"CANCELLED"}
if asset.id_type != "ACTION": if asset.id_type != 'ACTION':
self.report( # type: ignore self.report( # type: ignore
{"ERROR"}, {"ERROR"},
f"Selected asset {asset.name} is not an Action", f"Selected asset {asset.name} is not an Action",
@@ -462,12 +443,9 @@ class POSELIB_OT_pose_asset_select_bones(Operator):
def use_pose(self, context: Context, pose_asset: Action) -> Set[str]: def use_pose(self, context: Context, pose_asset: Action) -> Set[str]:
arm_object: Object = context.object arm_object: Object = context.object
#pose_usage.select_bones(arm_object, pose_asset, select=self.select, flipped=self.flipped) #pose_usage.select_bones(arm_object, pose_asset, select=self.select, flipped=self.flipped)
pose_usage.select_bones( pose_usage.select_bones(arm_object, pose_asset, selected_side=self.selected_side)
arm_object, pose_asset, selected_side=self.selected_side
)
return {"FINISHED"} return {"FINISHED"}
# This operator takes the Window Manager's `actionlib_flipped` property, and # This operator takes the Window Manager's `actionlib_flipped` property, and
# passes it to the `POSELIB_OT_blend_pose_asset` operator. This makes it # passes it to the `POSELIB_OT_blend_pose_asset` operator. This makes it
# possible to bind a key to the operator and still have it respect the global # possible to bind a key to the operator and still have it respect the global
@@ -500,14 +478,10 @@ class POSELIB_OT_blend_pose_asset_for_keymap(Operator):
""" """
def invoke(self, context: Context, event: Event) -> Set[str]: def invoke(self, context: Context, event: Event) -> Set[str]:
return bpy.ops.poselib.blend_pose_asset( return bpy.ops.poselib.blend_pose_asset(context.copy(), 'INVOKE_DEFAULT', flipped=self.flipped)
context.copy(), "INVOKE_DEFAULT", flipped=self.flipped
)
def execute(self, context: Context) -> Set[str]: def execute(self, context: Context) -> Set[str]:
return bpy.ops.poselib.blend_pose_asset( return bpy.ops.poselib.blend_pose_asset(context.copy(), 'EXEC_DEFAULT', flipped=self.flipped)
context.copy(), "EXEC_DEFAULT", flipped=self.flipped
)
# This operator takes the Window Manager's `actionlib_flipped` property, and # This operator takes the Window Manager's `actionlib_flipped` property, and
@@ -515,7 +489,6 @@ class POSELIB_OT_blend_pose_asset_for_keymap(Operator):
# possible to bind a key to the operator and still have it respect the global # possible to bind a key to the operator and still have it respect the global
# "Flip Pose" checkbox. # "Flip Pose" checkbox.
class POSELIB_OT_apply_pose_asset_for_keymap(Operator): class POSELIB_OT_apply_pose_asset_for_keymap(Operator):
bl_idname = "poselib.apply_pose_asset_for_keymap" bl_idname = "poselib.apply_pose_asset_for_keymap"
bl_options = {"REGISTER", "UNDO", "INTERNAL"} bl_options = {"REGISTER", "UNDO", "INTERNAL"}
@@ -523,7 +496,7 @@ class POSELIB_OT_apply_pose_asset_for_keymap(Operator):
_rna = bpy.ops.poselib.apply_pose_asset.get_rna_type() _rna = bpy.ops.poselib.apply_pose_asset.get_rna_type()
bl_label = _rna.name bl_label = _rna.name
#bl_description = _rna.description #bl_description = _rna.description
bl_description = "Apply Pose to Bones" bl_description = 'Apply Pose to Bones'
del _rna del _rna
flipped: BoolProperty(name="Flipped", default=False) # type: ignore flipped: BoolProperty(name="Flipped", default=False) # type: ignore
@@ -541,34 +514,19 @@ class POSELIB_OT_apply_pose_asset_for_keymap(Operator):
store_bones = {} store_bones = {}
bones = [ bones = [
"blendshape-eyes", 'blendshape-eyes', 'blendshape-eye.L', 'blendshape-eye.R',
"blendshape-eye.L", 'blendshape-corner-mouth', 'blendshape-corner-mouth.L',
"blendshape-eye.R", 'blendshape-corner-down-mouth.L', 'blendshape-corner-up-mouth.L',
"blendshape-corner-mouth", 'blendshape-corner-mouth-add.L','blendshape-corner-mouth.R',
"blendshape-corner-mouth.L", 'blendshape-corner-down-mouth.R', 'blendshape-corner-up-mouth.R',
"blendshape-corner-down-mouth.L", 'blendshape-corner-mouth-add.R', 'blendshape-center-up-mouth',
"blendshape-corner-up-mouth.L", 'blendshape-center-down-mouth',
"blendshape-corner-mouth-add.L", 'hat1.R', 'hat2.R', 'hat3.R', 'hat1.L', 'hat2.L', 'hat3.L',
"blendshape-corner-mouth.R",
"blendshape-corner-down-mouth.R",
"blendshape-corner-up-mouth.R",
"blendshape-corner-mouth-add.R",
"blendshape-center-up-mouth",
"blendshape-center-down-mouth",
"hat1.R",
"hat2.R",
"hat3.R",
"hat1.L",
"hat2.L",
"hat3.L",
] ]
attributes = [ attributes = [
"location", 'location', 'rotation_quaternion',
"rotation_quaternion", 'rotation_euler', 'rotation_axis_angle', 'scale'
"rotation_euler",
"rotation_axis_angle",
"scale",
] ]
if action: if action:
@@ -585,40 +543,30 @@ class POSELIB_OT_apply_pose_asset_for_keymap(Operator):
if not prop_name in store_bones[bone_name].keys(): if not prop_name in store_bones[bone_name].keys():
store_bones[bone_name][prop_name] = [] store_bones[bone_name][prop_name] = []
val = getattr( val = getattr(context.object.pose.bones[bone_name], prop_name)
context.object.pose.bones[bone_name], prop_name
)
store_bones[bone_name][prop_name].append( store_bones[bone_name][prop_name].append(fc.evaluate(context.scene.frame_current))
fc.evaluate(context.scene.frame_current)
)
bpy.ops.poselib.apply_pose_asset( bpy.ops.poselib.apply_pose_asset(context.copy(), 'EXEC_DEFAULT', flipped=True)
context.copy(), "EXEC_DEFAULT", flipped=True
)
for bone, v in store_bones.items(): for bone, v in store_bones.items():
for attr, attr_val in v.items(): for attr, attr_val in v.items():
flipped_vector = 1 flipped_vector = 1
### TODO FAIRE ÇA PROPREMENT AVEC UNE COMPREHENSION LIST OU AUTRE ### TODO FAIRE ÇA PROPREMENT AVEC UNE COMPREHENSION LIST OU AUTRE
if re.search(r"\.[RL]$", bone): if re.search(r'\.[RL]$', bone):
flipped_bone = pose_usage.flip_side_name(bone) flipped_bone = pose_usage.flip_side_name(bone)
if attr == "location": if attr == 'location':
flipped_vector = Vector((-1, 1, 1)) flipped_vector = Vector((-1, 1, 1))
# print('-----', store_bones.get(flipped_bone)[attr]) # print('-----', store_bones.get(flipped_bone)[attr])
attr_val = ( attr_val = Vector(store_bones.get(flipped_bone)[attr]) * flipped_vector
Vector(store_bones.get(flipped_bone)[attr]) * flipped_vector
)
setattr(context.object.pose.bones[bone], attr, attr_val) setattr(context.object.pose.bones[bone], attr, attr_val)
return {"FINISHED"} return {'FINISHED'}
else: else:
return bpy.ops.poselib.apply_pose_asset( return bpy.ops.poselib.apply_pose_asset(context.copy(), 'EXEC_DEFAULT', flipped=False)
context.copy(), "EXEC_DEFAULT", flipped=False
)
class POSELIB_OT_convert_old_poselib(Operator): class POSELIB_OT_convert_old_poselib(Operator):
@@ -629,18 +577,12 @@ class POSELIB_OT_convert_old_poselib(Operator):
@classmethod @classmethod
def poll(cls, context: Context) -> bool: def poll(cls, context: Context) -> bool:
action = ( action = context.object and context.object.animation_data and context.object.animation_data.action
context.object
and context.object.animation_data
and context.object.animation_data.action
)
if not action: if not action:
cls.poll_message_set("Active object has no Action") cls.poll_message_set("Active object has no Action")
return False return False
if not action.pose_markers: if not action.pose_markers:
cls.poll_message_set( cls.poll_message_set("Action %r is not a old-style pose library" % action.name)
"Action %r is not a old-style pose library" % action.name
)
return False return False
return True return True
@@ -651,11 +593,12 @@ class POSELIB_OT_convert_old_poselib(Operator):
new_actions = conversion.convert_old_poselib(old_poselib) new_actions = conversion.convert_old_poselib(old_poselib)
if not new_actions: if not new_actions:
self.report({"ERROR"}, "Unable to convert to pose assets") self.report({'ERROR'}, "Unable to convert to pose assets")
return {"CANCELLED"} return {'CANCELLED'}
self.report({'INFO'}, "Converted %d poses to pose assets" % len(new_actions))
return {'FINISHED'}
self.report({"INFO"}, "Converted %d poses to pose assets" % len(new_actions))
return {"FINISHED"}
classes = ( classes = (
@@ -666,7 +609,7 @@ classes = (
POSELIB_OT_create_pose_asset, POSELIB_OT_create_pose_asset,
POSELIB_OT_paste_asset, POSELIB_OT_paste_asset,
POSELIB_OT_pose_asset_select_bones, POSELIB_OT_pose_asset_select_bones,
POSELIB_OT_restore_previous_action, POSELIB_OT_restore_previous_action
) )
register, unregister = bpy.utils.register_classes_factory(classes) register, unregister = bpy.utils.register_classes_factory(classes)
@@ -129,9 +129,7 @@ class PoseActionCreator:
continue continue
try: try:
value = self._current_value( value = self._current_value(armature_ob, fcurve.data_path, fcurve.array_index)
armature_ob, fcurve.data_path, fcurve.array_index
)
except UnresolvablePathError: except UnresolvablePathError:
# A once-animated property no longer exists. # A once-animated property no longer exists.
continue continue
@@ -199,9 +197,7 @@ class PoseActionCreator:
fcurve: Optional[FCurve] = dst_action.fcurves.find(rna_path, index=array_index) fcurve: Optional[FCurve] = dst_action.fcurves.find(rna_path, index=array_index)
if fcurve is None: if fcurve is None:
fcurve = dst_action.fcurves.new( fcurve = dst_action.fcurves.new(rna_path, index=array_index, action_group=bone_name)
rna_path, index=array_index, action_group=bone_name
)
fcurve.keyframe_points.insert(self.params.src_frame_nr, value=value) fcurve.keyframe_points.insert(self.params.src_frame_nr, value=value)
fcurve.update() fcurve.update()
@@ -300,11 +296,8 @@ def create_pose_asset(
pose_action.asset_generate_preview() pose_action.asset_generate_preview()
return pose_action return pose_action
#def create_pose_asset_from_context(context: Context, new_asset_name: str, selection=True) -> Optional[Action]: #def create_pose_asset_from_context(context: Context, new_asset_name: str, selection=True) -> Optional[Action]:
def create_pose_asset_from_context( def create_pose_asset_from_context(context: Context, new_asset_name: str) -> Optional[Action]:
context: Context, new_asset_name: str
) -> Optional[Action]:
"""Create Action asset from active object & selected bones.""" """Create Action asset from active object & selected bones."""
bones = context.selected_pose_bones_from_active_object bones = context.selected_pose_bones_from_active_object
@@ -376,10 +369,7 @@ def copy_keyframe(dst_fcurve: FCurve, src_keyframe: Keyframe) -> Keyframe:
"""Copy a keyframe from one FCurve to the other.""" """Copy a keyframe from one FCurve to the other."""
dst_keyframe = dst_fcurve.keyframe_points.insert( dst_keyframe = dst_fcurve.keyframe_points.insert(
src_keyframe.co.x, src_keyframe.co.x, src_keyframe.co.y, options={'FAST'}, keyframe_type=src_keyframe.type
src_keyframe.co.y,
options={"FAST"},
keyframe_type=src_keyframe.type,
) )
for propname in { for propname in {
@@ -422,9 +412,7 @@ def find_keyframe(fcurve: FCurve, frame: float) -> Optional[Keyframe]:
return None return None
def assign_from_asset_browser( def assign_from_asset_browser(asset: Action, asset_browser_area: bpy.types.Area) -> None:
asset: Action, asset_browser_area: bpy.types.Area
) -> None:
"""Assign some things from the asset browser to the asset. """Assign some things from the asset browser to the asset.
This sets the current catalog ID, and in the future could include tags This sets the current catalog ID, and in the future could include tags
@@ -35,14 +35,15 @@ def select_bones(arm_object: Object, action: Action, *, selected_side, toggle=Tr
continue continue
seen_bone_names.add(bone_name) seen_bone_names.add(bone_name)
if selected_side == "FLIPPED": if selected_side == 'FLIPPED':
bones_to_select.add(bone_name_flip) bones_to_select.add(bone_name_flip)
elif selected_side == "BOTH": elif selected_side == 'BOTH':
bones_to_select.add(bone_name_flip) bones_to_select.add(bone_name_flip)
bones_to_select.add(bone_name) bones_to_select.add(bone_name)
elif selected_side == "CURRENT": elif selected_side == 'CURRENT':
bones_to_select.add(bone_name) bones_to_select.add(bone_name)
for bone in bones_to_select: for bone in bones_to_select:
pose_bone = pose.bones.get(bone) pose_bone = pose.bones.get(bone)
if pose_bone: if pose_bone:
@@ -173,7 +174,7 @@ def flip_side_name(to_flip: str) -> str:
return prefix + replace + suffix + number return prefix + replace + suffix + number
if __name__ == "__main__": if __name__ == '__main__':
import doctest import doctest
print(f"Test result: {doctest.testmod()}") print(f"Test result: {doctest.testmod()}")
-177
View File
@@ -1,177 +0,0 @@
import argparse
import sys
import json
from pathlib import Path
import bpy
import re
import uuid
from itertools import groupby
from asset_library.constants import ASSETLIB_FILENAME, MODULE_DIR
from asset_library.common.bl_utils import thumbnail_blend_file
from asset_library.common.functions import command
@command
def bundle_library(
source_directory,
bundle_directory,
template_info,
thumbnail_template,
template=None,
data_file=None,
):
field_pattern = r"{(\w+)}"
asset_data_path = Path(bundle_directory) / ASSETLIB_FILENAME
glob_pattern = re.sub(field_pattern, "*", template)
re_pattern = re.sub(field_pattern, r"([\\w -_.]+)", template)
re_pattern = re_pattern.replace("?", ".")
field_names = re.findall(field_pattern, template)
asset_file_datas = []
for f in sorted(Path(source_directory).glob(glob_pattern)):
rel_path = f.relative_to(source_directory).as_posix()
field_values = re.findall(re_pattern, rel_path)[0]
field_data = {k: v for k, v in zip(field_names, field_values)}
name = field_data.get("name", f.stem)
thumbnail = (f / thumbnail_template.format(name=name)).resolve()
asset_data = (f / template_info.format(name=name)).resolve()
catalogs = sorted(
[v for k, v in sorted(field_data.items()) if re.findall("cat[0-9]+", k)]
)
catalogs = [c.replace("_", " ").title() for c in catalogs]
if not thumbnail.exists():
thumbnail_blend_file(f, thumbnail)
asset_data = {
"catalog": "/".join(catalogs),
"preview": thumbnail.as_posix(), #'./' + bpy.path.relpath(str(thumbnail), start=str(f))[2:],
"filepath": f.as_posix(), #'./' + bpy.path.relpath(str(f), start=str(asset_data_path))[2:],
"name": name,
"tags": [],
"metadata": {"filepath": f.as_posix()},
}
asset_file_datas.append(asset_data)
# Write json data file to store all asset found
print(f"Writing asset data file to, {asset_data_path}")
asset_data_path.write_text(json.dumps(asset_file_datas, indent=4))
# script = MODULE_DIR / 'common' / 'bundle_blend.py'
# cmd = [bpy.app.binary_path, '--python', str(script), '--', '--filepath', str(filepath)]
# print(cmd)
# subprocess.call(cmd)
@command
def bundle_blend(filepath, depth=0):
# print('Bundle Blend...')
filepath = Path(filepath)
# asset_data_path = get_asset_datas_file(filepath)
asset_data_path = filepath / ASSETLIB_FILENAME
blend_name = filepath.name.replace(" ", "_").lower()
blend_path = (filepath / blend_name).with_suffix(".blend")
if not asset_data_path.exists():
raise Exception(f"The file {asset_data_path} not exist")
catalog_path = get_catalog_path(filepath)
catalog_data = read_catalog(catalog_path)
asset_file_data = json.loads(asset_data_path.read_text())
# asset_file_data = {i['catalog']:i for i in asset_file_data}
if depth == 0:
groups = [asset_file_data]
else:
asset_file_data.sort(key=lambda x: x["catalog"].split("/")[:depth])
groups = groupby(asset_file_data, key=lambda x: x["catalog"].split("/")[:depth])
# progress = 0
total_assets = len(asset_file_data)
i = 0
for sub_path, asset_datas in groups:
bpy.ops.wm.read_homefile(use_empty=True)
for asset_data in asset_datas:
blend_name = sub_path[-1].replace(" ", "_").lower()
blend_path = Path(filepath, *sub_path, blend_name).with_suffix(".blend")
if i % int(total_assets / 100) == 0:
print(f"Progress: {int(i / total_assets * 100)}")
col = bpy.data.collections.new(name=asset_data["name"])
# Seems slow
# bpy.context.scene.collection.children.link(col)
col.asset_mark()
with bpy.context.temp_override(id=col):
bpy.ops.ed.lib_id_load_custom_preview(filepath=asset_data["preview"])
col.asset_data.description = asset_data.get("description", "")
catalog_name = asset_data["catalog"]
catalog = catalog_data.get(catalog_name)
if not catalog:
catalog = {"id": str(uuid.uuid4()), "name": catalog_name}
catalog_data[catalog_name] = catalog
col.asset_data.catalog_id = catalog["id"]
for k, v in asset_data.get("metadata", {}).items():
col.asset_data[k] = v
i += 1
print(f"Saving Blend to {blend_path}")
blend_path.mkdir(exist_ok=True, parents=True)
bpy.ops.wm.save_as_mainfile(filepath=str(blend_path), compress=True)
write_catalog(catalog_path, catalog_data)
bpy.ops.wm.quit_blender()
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="bundle_blend",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
parser.add_argument("--source-path")
parser.add_argument("--bundle-path")
parser.add_argument("--asset-data-template")
parser.add_argument("--thumbnail-template")
parser.add_argument("--template", default=None)
parser.add_argument("--data-file", default=None)
parser.add_argument("--depth", default=0, type=int)
if "--" in sys.argv:
index = sys.argv.index("--")
sys.argv = [sys.argv[index - 1], *sys.argv[index + 1 :]]
args = parser.parse_args()
bundle_library(
source_directory=args.source_directory,
bundle_directory=args.bundle_directory,
template_info=args.template_info,
thumbnail_template=args.thumbnail_template,
template=args.template,
data_file=args.data_file,
)
bundle_blend(filepath=args.bundle_directory, depth=args.depth)
Binary file not shown.
-343
View File
@@ -1,343 +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)
-68
View File
@@ -1,68 +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()
-16
View File
@@ -1,16 +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
-41
View File
@@ -1,41 +0,0 @@
"""
Adapter for making an asset library of all blender file found in a folder
"""
from asset_library.library_types.library_type import LibraryType
from asset_library.common.file_utils import copy_dir
from bpy.props import StringProperty
from os.path import expandvars
import bpy
class CopyFolder(LibraryType):
"""Copy library folder from a server to a local disk for better performance"""
name = "Copy Folder"
source_directory: StringProperty()
includes: StringProperty()
excludes: StringProperty()
def bundle(self, cache_diff=None):
src = expandvars(self.source_directory)
dst = expandvars(self.bundle_directory)
includes = [inc.strip() for inc in self.includes.split(",")]
excludes = [ex.strip() for ex in self.excludes.split(",")]
print(f"Copy Folder from {src} to {dst}...")
copy_dir(src, dst, only_recent=True, excludes=excludes, includes=includes)
def filter_prop(self, prop):
if prop in ("template_info", "template_video", "template_image", "blend_depth"):
return False
return True
# def draw_prop(self, layout, prop):
# if prop in ('template_info', 'template_video', 'template_image', 'blend_depth'):
# return
# super().draw_prop(layout)
-284
View File
@@ -1,284 +0,0 @@
"""
Plugin for making an asset library of all blender file found in a folder
"""
from asset_library.library_types.library_type import LibraryType
from asset_library.common.template import Template
from asset_library.common.file_utils import install_module
import bpy
from bpy.props import StringProperty, IntProperty, BoolProperty
import re
from pathlib import Path
from itertools import groupby
import uuid
import os
import shutil
import json
import urllib3
import traceback
import time
class Kitsu(LibraryType):
name = "Kitsu"
template_name: StringProperty()
template_file: StringProperty()
source_directory: StringProperty(subtype="DIR_PATH")
# blend_depth: IntProperty(default=1)
source_template_image: StringProperty()
target_template_image: StringProperty()
url: StringProperty()
login: StringProperty()
password: StringProperty(subtype="PASSWORD")
project_name: StringProperty()
def connect(self, url=None, login=None, password=None):
"""Connect to kitsu api using provided url, login and password"""
gazu = install_module("gazu")
urllib3.disable_warnings()
if not self.url:
print(f"Kitsu Url: {self.url} is empty")
return
url = self.url
if not url.endswith("/api"):
url += "/api"
print(f"Info: Setting Host for kitsu {url}")
gazu.client.set_host(url)
if not gazu.client.host_is_up():
print("Error: Kitsu Host is down")
try:
print(f"Info: Log in to kitsu as {self.login}")
res = gazu.log_in(self.login, self.password)
print(f'Info: Sucessfully login to Kitsu as {res["user"]["full_name"]}')
return res["user"]
except Exception as e:
print(f"Error: {traceback.format_exc()}")
def get_asset_path(self, name, catalog, directory=None):
directory = directory or self.source_directory
return Path(directory, self.get_asset_relative_path(name, catalog))
def get_asset_info(self, data, asset_path):
modified = time.time_ns()
catalog = data["entity_type_name"].title()
asset_path = self.prop_rel_path(asset_path, "source_directory")
# asset_name = self.norm_file_name(data['name'])
asset_info = dict(
filepath=asset_path,
modified=modified,
library_id=self.library.id,
assets=[
dict(
catalog=catalog,
metadata=data.get("data", {}),
description=data["description"],
tags=[],
type=self.data_type,
# image=self.library.template_image,
# video=self.library.template_video,
name=data["name"],
)
],
)
return asset_info
# def bundle(self, cache_diff=None):
# """Group all asset in one or multiple blends for the asset browser"""
# return super().bundle(cache_diff=cache_diff)
def set_asset_preview(self, asset, asset_data):
"""Load an externalize image as preview for an asset using the source template"""
asset_path = self.format_path(Path(asset_data["filepath"]).as_posix())
image_path = self.find_path(
self.target_template_image, asset_data, filepath=asset_path
)
if image_path:
with bpy.context.temp_override(id=asset):
bpy.ops.ed.lib_id_load_custom_preview(filepath=str(image_path))
else:
print(f"No image found for {self.target_template_image} on {asset.name}")
if asset.preview:
return asset.preview
def generate_previews(self, cache=None):
print("Generate previews...")
if cache in (None, ""):
cache = self.fetch()
elif isinstance(cache, (Path, str)):
cache = self.read_cache(cache)
# TODO Support all multiple data_type
for asset_info in cache:
if asset_info.get("type", self.data_type) == "FILE":
self.generate_blend_preview(asset_info)
else:
self.generate_asset_preview(asset_info)
def generate_asset_preview(self, asset_info):
data_type = self.data_type
scn = bpy.context.scene
vl = bpy.context.view_layer
asset_path = self.format_path(asset_info["filepath"])
lens = 85
if not asset_path.exists():
print(f"Blend file {asset_path} not exit")
return
asset_data_names = {}
# First check wich assets need a preview
for asset_data in asset_info["assets"]:
name = asset_data["name"]
image_path = self.format_path(
self.target_template_image, asset_data, filepath=asset_path
)
if image_path.exists():
continue
# Store in a dict all asset_data that does not have preview
asset_data_names[name] = dict(asset_data, image_path=image_path)
if not asset_data_names:
print(f"All previews already existing for {asset_path}")
return
# asset_names = [a['name'] for a in asset_info['assets']]
asset_names = list(asset_data_names.keys())
assets = self.load_datablocks(
asset_path, names=asset_names, link=True, type=data_type
)
print(asset_names)
print(assets)
for asset in assets:
if not asset:
continue
print(f"Generate Preview for asset {asset.name}")
asset_data = asset_data_names[asset.name]
# print(self.target_template_image, asset_path)
image_path = self.format_path(
self.target_template_image, asset_data, filepath=asset_path
)
# Force redo preview
# if asset.preview:
# print(f'Writing asset preview to {image_path}')
# self.write_preview(asset.preview, image_path)
# continue
if data_type == "COLLECTION":
bpy.ops.object.collection_instance_add(name=asset.name)
scn.camera.data.lens = lens
bpy.ops.view3d.camera_to_view_selected()
scn.camera.data.lens -= 5
instance = vl.objects.active
# scn.collection.children.link(asset)
scn.render.filepath = str(image_path)
scn.render.image_settings.file_format = self.format_from_ext(
image_path.suffix
)
scn.render.image_settings.color_mode = "RGBA"
scn.render.image_settings.quality = 90
print(f"Render asset {asset.name} to {image_path}")
bpy.ops.render.render(write_still=True)
# instance.user_clear()
asset.user_clear()
bpy.data.objects.remove(instance)
bpy.ops.outliner.orphans_purge(
do_local_ids=True, do_linked_ids=True, do_recursive=True
)
def fetch(self):
"""Gather in a list all assets found in the folder"""
print(f"Fetch Assets for {self.library.name}")
gazu = install_module("gazu")
self.connect()
template_file = Template(self.template_file)
template_name = Template(self.template_name)
project = gazu.client.fetch_first("projects", {"name": self.project_name})
entity_types = gazu.client.fetch_all("entity-types")
entity_types_ids = {e["id"]: e["name"] for e in entity_types}
cache = self.read_cache()
for asset_data in gazu.asset.all_assets_for_project(project):
asset_data["entity_type_name"] = entity_types_ids[
asset_data.pop("entity_type_id")
]
asset_name = asset_data["name"]
asset_field_data = dict(
asset_name=asset_name,
type=asset_data["entity_type_name"],
source_directory=self.source_directory,
)
try:
asset_field_data.update(template_name.parse(asset_name))
except Exception:
print(
f"Warning: Could not parse {asset_name} with template {template_name}"
)
asset_path = template_file.find(asset_field_data)
if not asset_path:
print(
f"Warning: Could not find file for {template_file.format(asset_field_data)}"
)
continue
asset_path = self.prop_rel_path(asset_path, "source_directory")
asset_cache_data = dict(
catalog=asset_data["entity_type_name"].title(),
metadata=asset_data.get("data", {}),
description=asset_data["description"],
tags=[],
type=self.data_type,
name=asset_data["name"],
)
cache.add_asset_cache(asset_cache_data, filepath=asset_path)
return cache
+538 -708
View File
File diff suppressed because it is too large Load Diff
View File
+45 -54
View File
@@ -1,13 +1,15 @@
""" """
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.scan_folder import ScanFolder
from asset_library.common.bl_utils import load_datablocks from asset_library.plugins.scan_folder import ScanFolder
from asset_library.common.template import Template from asset_library.core.bl_utils import load_datablocks
from asset_library.core.template import Template
import bpy import bpy
from bpy.props import StringProperty, IntProperty, BoolProperty from bpy.props import (StringProperty, IntProperty, BoolProperty)
import re import re
from pathlib import Path from pathlib import Path
from itertools import groupby from itertools import groupby
@@ -22,7 +24,7 @@ from pprint import pprint
class Conform(ScanFolder): class Conform(ScanFolder):
name = "Conform" name = "Conform"
source_directory: StringProperty(subtype="DIR_PATH") source_directory : StringProperty(subtype='DIR_PATH')
target_template_file : StringProperty() target_template_file : StringProperty()
target_template_info : StringProperty() target_template_info : StringProperty()
@@ -33,18 +35,16 @@ class Conform(ScanFolder):
layout.prop(self, "source_directory", text="Source : Directory") layout.prop(self, "source_directory", text="Source : Directory")
col = layout.column(align=True) col = layout.column(align=True)
col.prop(self, "source_template_file", icon="COPY_ID", text="Template file") col.prop(self, "source_template_file", icon='COPY_ID', text='Template file')
col.prop(self, "source_template_image", icon="COPY_ID", text="Template image") col.prop(self, "source_template_image", icon='COPY_ID', text='Template image')
col.prop(self, "source_template_video", icon="COPY_ID", text="Template video") col.prop(self, "source_template_video", icon='COPY_ID', text='Template video')
col.prop(self, "source_template_info", icon="COPY_ID", text="Template info") col.prop(self, "source_template_info", icon='COPY_ID', text='Template info')
col = layout.column(align=True) col = layout.column(align=True)
col.prop( col.prop(self, "target_template_file", icon='COPY_ID', text='Target : Template file')
self, "target_template_file", icon="COPY_ID", text="Target : Template file" col.prop(self, "target_template_image", icon='COPY_ID', text='Template image')
) col.prop(self, "target_template_video", icon='COPY_ID', text='Template video')
col.prop(self, "target_template_image", icon="COPY_ID", text="Template image") col.prop(self, "target_template_info", icon='COPY_ID', text='Template info')
col.prop(self, "target_template_video", icon="COPY_ID", text="Template video")
col.prop(self, "target_template_info", icon="COPY_ID", text="Template info")
def get_asset_bundle_path(self, asset_data): def get_asset_bundle_path(self, asset_data):
"""Template file are relative""" """Template file are relative"""
@@ -52,30 +52,26 @@ class Conform(ScanFolder):
src_directory = Path(self.source_directory).resolve() src_directory = Path(self.source_directory).resolve()
src_template_file = Template(self.source_template_file) src_template_file = Template(self.source_template_file)
asset_path = Path(asset_data["filepath"]).as_posix() asset_path = Path(asset_data['filepath']).as_posix()
asset_path = self.format_path(asset_path) asset_path = self.format_path(asset_path)
rel_path = asset_path.relative_to(src_directory).as_posix() rel_path = asset_path.relative_to(src_directory).as_posix()
field_data = src_template_file.parse(rel_path) field_data = src_template_file.parse(rel_path)
#field_data = {f"catalog_{k}": v for k, v in field_data.items()} #field_data = {f"catalog_{k}": v for k, v in field_data.items()}
# Change the int in the template by string to allow format # Change the int in the template by string to allow format
#target_template_file = re.sub(r'{(\d+)}', r'{cat\1}', self.target_template_file) #target_template_file = re.sub(r'{(\d+)}', r'{cat\1}', self.target_template_file)
format_data = self.format_asset_data(asset_data) format_data = self.format_asset_data(asset_data)
#format_data['asset_name'] = format_data['asset_name'].lower().replace(' ', '_') #format_data['asset_name'] = format_data['asset_name'].lower().replace(' ', '_')
path = ( path = Template(self.target_template_file).format(format_data, **field_data).with_suffix('.blend')
Template(self.target_template_file)
.format(format_data, **field_data)
.with_suffix(".blend")
)
path = Path(self.bundle_directory, path).resolve() path = Path(self.bundle_directory, path).resolve()
return path return path
def set_asset_preview(self, asset, asset_data): def set_asset_preview(self, asset, asset_data):
"""Load an externalize image as preview for an asset using the target template""" '''Load an externalize image as preview for an asset using the target template'''
image_template = self.target_template_image image_template = self.target_template_image
if not image_template: if not image_template:
@@ -86,16 +82,18 @@ class Conform(ScanFolder):
if image_path: if image_path:
with bpy.context.temp_override(id=asset): with bpy.context.temp_override(id=asset):
bpy.ops.ed.lib_id_load_custom_preview(filepath=str(image_path)) bpy.ops.ed.lib_id_load_custom_preview(
filepath=str(image_path)
)
else: else:
print(f"No image found for {image_template} on {asset.name}") print(f'No image found for {image_template} on {asset.name}')
if asset.preview: if asset.preview:
return asset.preview return asset.preview
def generate_previews(self, cache_diff): def generate_previews(self, cache_diff):
print("Generate previews...") print('Generate previews...')
# if cache in (None, ''): # if cache in (None, ''):
# cache = self.fetch() # cache = self.fetch()
@@ -105,10 +103,12 @@ class Conform(ScanFolder):
if isinstance(cache, (Path, str)): if isinstance(cache, (Path, str)):
cache_diff = LibraryCacheDiff(cache_diff) cache_diff = LibraryCacheDiff(cache_diff)
#TODO Support all multiple data_type #TODO Support all multiple data_type
for asset_info in cache: for asset_info in cache:
if asset_info.get("type", self.data_type) == "FILE": if asset_info.get('type', self.data_type) == 'FILE':
self.generate_blend_preview(asset_info) self.generate_blend_preview(asset_info)
else: else:
self.generate_asset_preview(asset_info) self.generate_asset_preview(asset_info)
@@ -124,47 +124,41 @@ class Conform(ScanFolder):
#camera = scn.camera #camera = scn.camera
data_type = self.data_type #asset_info['data_type'] data_type = self.data_type #asset_info['data_type']
asset_path = self.format_path(asset_info["filepath"]) asset_path = self.format_path(asset_info['filepath'])
# Check if a source video exists and if so copying it in the new directory # Check if a source video exists and if so copying it in the new directory
if self.source_template_video and self.target_template_video: if self.source_template_video and self.target_template_video:
for asset_data in asset_info["assets"]: for asset_data in asset_info['assets']:
asset_data = dict(asset_data, filepath=asset_path) asset_data = dict(asset_data, filepath=asset_path)
dst_asset_path = self.get_asset_bundle_path(asset_data) dst_asset_path = self.get_asset_bundle_path(asset_data)
dst_video_path = self.format_path( dst_video_path = self.format_path(self.target_template_video, asset_data, filepath=dst_asset_path)
self.target_template_video, asset_data, filepath=dst_asset_path
)
if dst_video_path.exists(): if dst_video_path.exists():
print(f"The dest video {dst_video_path} already exist") print(f'The dest video {dst_video_path} already exist')
continue continue
src_video_path = self.find_path(self.source_template_video, asset_data) src_video_path = self.find_path(self.source_template_video, asset_data)
if src_video_path: if src_video_path:
print(f"Copy video from {src_video_path} to {dst_video_path}") print(f'Copy video from {src_video_path} to {dst_video_path}')
self.copy_file(src_video_path, dst_video_path) self.copy_file(src_video_path, dst_video_path)
# Check if asset as a preview image or need it to be generated # Check if asset as a preview image or need it to be generated
asset_data_names = {} asset_data_names = {}
if self.target_template_image: if self.target_template_image:
for asset_data in asset_info["assets"]: for asset_data in asset_info['assets']:
asset_data = dict(asset_data, filepath=asset_path) asset_data = dict(asset_data, filepath=asset_path)
name = asset_data["name"] name = asset_data['name']
dst_asset_path = self.get_asset_bundle_path(asset_data) dst_asset_path = self.get_asset_bundle_path(asset_data)
dst_image_path = self.format_path( dst_image_path = self.format_path(self.target_template_image, asset_data, filepath=dst_asset_path)
self.target_template_image, asset_data, filepath=dst_asset_path
)
if dst_image_path.exists(): if dst_image_path.exists():
print(f"The dest image {dst_image_path} already exist") print(f'The dest image {dst_image_path} already exist')
continue continue
# Check if a source image exists and if so copying it in the new directory # Check if a source image exists and if so copying it in the new directory
if self.source_template_image: if self.source_template_image:
src_image_path = self.find_path( src_image_path = self.find_path(self.source_template_image, asset_data)
self.source_template_image, asset_data
)
if src_image_path: if src_image_path:
if src_image_path.suffix == dst_image_path.suffix: if src_image_path.suffix == dst_image_path.suffix:
@@ -179,29 +173,28 @@ class Conform(ScanFolder):
#Store in a dict all asset_data that does not have preview #Store in a dict all asset_data that does not have preview
asset_data_names[name] = dict(asset_data, image_path=dst_image_path) asset_data_names[name] = dict(asset_data, image_path=dst_image_path)
if not asset_data_names:# No preview to generate if not asset_data_names:# No preview to generate
return return
print("Making Preview for", list(asset_data_names.keys())) print('Making Preview for', list(asset_data_names.keys()))
asset_names = list(asset_data_names.keys()) asset_names = list(asset_data_names.keys())
assets = self.load_datablocks( assets = self.load_datablocks(asset_path, names=asset_names, link=True, type=data_type)
asset_path, names=asset_names, link=True, type=data_type
)
for asset in assets: for asset in assets:
if not asset: if not asset:
continue continue
asset_data = asset_data_names[asset.name] asset_data = asset_data_names[asset.name]
image_path = asset_data["image_path"] image_path = asset_data['image_path']
if asset.preview: if asset.preview:
print(f"Writing asset preview to {image_path}") print(f'Writing asset preview to {image_path}')
self.write_preview(asset.preview, image_path) self.write_preview(asset.preview, image_path)
continue continue
if data_type == "COLLECTION": if data_type == 'COLLECTION':
bpy.ops.object.collection_instance_add(name=asset.name) bpy.ops.object.collection_instance_add(name=asset.name)
@@ -212,7 +205,7 @@ class Conform(ScanFolder):
scn.render.filepath = str(image_path) scn.render.filepath = str(image_path)
print(f"Render asset {asset.name} to {image_path}") print(f'Render asset {asset.name} to {image_path}')
bpy.ops.render.render(write_still=True) bpy.ops.render.render(write_still=True)
#instance.user_clear() #instance.user_clear()
@@ -220,6 +213,4 @@ class Conform(ScanFolder):
bpy.data.objects.remove(instance) bpy.data.objects.remove(instance)
bpy.ops.outliner.orphans_purge( bpy.ops.outliner.orphans_purge(do_local_ids=True, do_linked_ids=True, do_recursive=True)
do_local_ids=True, do_linked_ids=True, do_recursive=True
)
+49
View File
@@ -0,0 +1,49 @@
"""
Adapter for making an asset library of all blender file found in a folder
"""
from os.path import expandvars
import bpy
from bpy.props import StringProperty
from asset_library.plugins.library_plugin import LibraryPlugin
from asset_library.core.file_utils import copy_dir
class CopyFolder(LibraryPlugin):
"""Copy library folder from a server to a local disk for better performance"""
name = "Copy Folder"
source_directory : StringProperty()
includes : StringProperty()
excludes : StringProperty()
def bundle(self, cache_diff=None):
src = expandvars(self.source_directory)
dst = expandvars(self.bundle_directory)
includes = [inc.strip() for inc in self.includes.split(',')]
excludes = [ex.strip() for ex in self.excludes.split(',')]
print(f'Copy Folder from {src} to {dst}...')
copy_dir(
src, dst, only_recent=True,
excludes=excludes, includes=includes
)
def filter_prop(self, prop):
if prop in ('template_info', 'template_video', 'template_image', 'blend_depth'):
return False
return True
# def draw_prop(self, layout, prop):
# if prop in ('template_info', 'template_video', 'template_image', 'blend_depth'):
# return
# super().draw_prop(layout)
View File
+269
View File
@@ -0,0 +1,269 @@
"""
Plugin for making an asset library of all blender file found in a folder
"""
import re
from pathlib import Path
from itertools import groupby
import uuid
import os
import shutil
import json
import urllib3
import traceback
import time
import bpy
from bpy.props import (StringProperty, IntProperty, BoolProperty)
from asset_library.plugins.library_plugin import LibraryPlugin
from asset_library.core.template import Template
from asset_library.core.file_utils import install_module
class Kitsu(LibraryPlugin):
name = "Kitsu"
template_name : StringProperty()
template_file : StringProperty()
source_directory : StringProperty(subtype='DIR_PATH')
#blend_depth: IntProperty(default=1)
source_template_image : StringProperty()
target_template_image : StringProperty()
url: StringProperty()
login: StringProperty()
password: StringProperty(subtype='PASSWORD')
project_name: StringProperty()
def connect(self, url=None, login=None, password=None):
'''Connect to kitsu api using provided url, login and password'''
gazu = install_module('gazu')
urllib3.disable_warnings()
if not self.url:
print(f'Kitsu Url: {self.url} is empty')
return
url = self.url
if not url.endswith('/api'):
url += '/api'
print(f'Info: Setting Host for kitsu {url}')
gazu.client.set_host(url)
if not gazu.client.host_is_up():
print('Error: Kitsu Host is down')
try:
print(f'Info: Log in to kitsu as {self.login}')
res = gazu.log_in(self.login, self.password)
print(f'Info: Sucessfully login to Kitsu as {res["user"]["full_name"]}')
return res['user']
except Exception as e:
print(f'Error: {traceback.format_exc()}')
def get_asset_path(self, name, catalog, directory=None):
directory = directory or self.source_directory
return Path(directory, self.get_asset_relative_path(name, catalog))
def get_asset_info(self, data, asset_path):
modified = time.time_ns()
catalog = data['entity_type_name'].title()
asset_path = self.prop_rel_path(asset_path, 'source_directory')
#asset_name = self.norm_file_name(data['name'])
asset_info = dict(
filepath=asset_path,
modified=modified,
library_id=self.library.id,
assets=[dict(
catalog=catalog,
metadata=data.get('data', {}),
description=data['description'],
tags=[],
type=self.data_type,
#image=self.library.template_image,
#video=self.library.template_video,
name=data['name'])
]
)
return asset_info
# def bundle(self, cache_diff=None):
# """Group all asset in one or multiple blends for the asset browser"""
# return super().bundle(cache_diff=cache_diff)
def set_asset_preview(self, asset, asset_data):
'''Load an externalize image as preview for an asset using the source template'''
asset_path = self.format_path(Path(asset_data['filepath']).as_posix())
image_path = self.find_path(self.target_template_image, asset_data, filepath=asset_path)
if image_path:
with bpy.context.temp_override(id=asset):
bpy.ops.ed.lib_id_load_custom_preview(
filepath=str(image_path)
)
else:
print(f'No image found for {self.target_template_image} on {asset.name}')
if asset.preview:
return asset.preview
def generate_previews(self, cache=None):
print('Generate previews...')
if cache in (None, ''):
cache = self.fetch()
elif isinstance(cache, (Path, str)):
cache = self.read_cache(cache)
#TODO Support all multiple data_type
for asset_info in cache:
if asset_info.get('type', self.data_type) == 'FILE':
self.generate_blend_preview(asset_info)
else:
self.generate_asset_preview(asset_info)
def generate_asset_preview(self, asset_info):
data_type = self.data_type
scn = bpy.context.scene
vl = bpy.context.view_layer
asset_path = self.format_path(asset_info['filepath'])
lens = 85
if not asset_path.exists():
print(f'Blend file {asset_path} not exit')
return
asset_data_names = {}
# First check wich assets need a preview
for asset_data in asset_info['assets']:
name = asset_data['name']
image_path = self.format_path(self.target_template_image, asset_data, filepath=asset_path)
if image_path.exists():
continue
#Store in a dict all asset_data that does not have preview
asset_data_names[name] = dict(asset_data, image_path=image_path)
if not asset_data_names:
print(f'All previews already existing for {asset_path}')
return
#asset_names = [a['name'] for a in asset_info['assets']]
asset_names = list(asset_data_names.keys())
assets = self.load_datablocks(asset_path, names=asset_names, link=True, type=data_type)
print(asset_names)
print(assets)
for asset in assets:
if not asset:
continue
print(f'Generate Preview for asset {asset.name}')
asset_data = asset_data_names[asset.name]
#print(self.target_template_image, asset_path)
image_path = self.format_path(self.target_template_image, asset_data, filepath=asset_path)
# Force redo preview
# if asset.preview:
# print(f'Writing asset preview to {image_path}')
# self.write_preview(asset.preview, image_path)
# continue
if data_type == 'COLLECTION':
bpy.ops.object.collection_instance_add(name=asset.name)
scn.camera.data.lens = lens
bpy.ops.view3d.camera_to_view_selected()
scn.camera.data.lens -= 5
instance = vl.objects.active
#scn.collection.children.link(asset)
scn.render.filepath = str(image_path)
scn.render.image_settings.file_format = self.format_from_ext(image_path.suffix)
scn.render.image_settings.color_mode = 'RGBA'
scn.render.image_settings.quality = 90
print(f'Render asset {asset.name} to {image_path}')
bpy.ops.render.render(write_still=True)
#instance.user_clear()
asset.user_clear()
bpy.data.objects.remove(instance)
bpy.ops.outliner.orphans_purge(do_local_ids=True, do_linked_ids=True, do_recursive=True)
def fetch(self):
"""Gather in a list all assets found in the folder"""
print(f'Fetch Assets for {self.library.name}')
gazu = install_module('gazu')
self.connect()
template_file = Template(self.template_file)
template_name = Template(self.template_name)
project = gazu.client.fetch_first('projects', {'name': self.project_name})
entity_types = gazu.client.fetch_all('entity-types')
entity_types_ids = {e['id']: e['name'] for e in entity_types}
cache = self.read_cache()
for asset_data in gazu.asset.all_assets_for_project(project):
asset_data['entity_type_name'] = entity_types_ids[asset_data.pop('entity_type_id')]
asset_name = asset_data['name']
asset_field_data = dict(asset_name=asset_name, type=asset_data['entity_type_name'], source_directory=self.source_directory)
try:
asset_field_data.update(template_name.parse(asset_name))
except Exception:
print(f'Warning: Could not parse {asset_name} with template {template_name}')
asset_path = template_file.find(asset_field_data)
if not asset_path:
print(f'Warning: Could not find file for {template_file.format(asset_field_data)}')
continue
asset_path = self.prop_rel_path(asset_path, 'source_directory')
asset_cache_data = dict(
catalog=asset_data['entity_type_name'].title(),
metadata=asset_data.get('data', {}),
description=asset_data['description'],
tags=[],
type=self.data_type,
name=asset_data['name']
)
cache.add_asset_cache(asset_cache_data, filepath=asset_path)
return cache
@@ -1,41 +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 asset_library.core.bl_utils import get_addon_prefs, load_datablocks
from asset_library.core.file_utils import read_file, write_file
from asset_library.core.template import Template
from asset_library.constants import (MODULE_DIR, RESOURCES_DIR)
#from asset_library.data_type import (action, collection, file)
#from asset_library.common.library_cache import LibraryCacheDiff
class LibraryPlugin(PropertyGroup):
#def __init__(self): #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
@@ -81,28 +83,20 @@ class LibraryType(PropertyGroup):
@property @property
def module_type(self): def module_type(self):
lib_type = self.library.data_type lib_type = self.library.data_type
if lib_type == "ACTION": if lib_type == 'ACTION':
return action return action
elif lib_type == "FILE": elif lib_type == 'FILE':
return file return file
elif lib_type == "COLLECTION": elif lib_type == 'COLLECTION':
return collection return collection
@property @property
def format_data(self): def format_data(self):
"""Dict for formating template""" """Dict for formating template"""
return dict( return dict(self.to_dict(), bundle_dir=self.library.bundle_dir, parent=self.library.parent)
self.to_dict(),
bundle_dir=self.library.bundle_dir,
parent=self.library.parent,
)
def to_dict(self): def to_dict(self):
return { return {p: getattr(self, p) for p in self.bl_rna.properties.keys() if p !='rna_type'}
p: getattr(self, p)
for p in self.bl_rna.properties.keys()
if p != "rna_type"
}
def read_catalog(self): def read_catalog(self):
return self.library.read_catalog() return self.library.read_catalog()
@@ -111,10 +105,10 @@ 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(' ', '_')
def read_file(self, file): def read_file(self, file):
return read_file(file) return read_file(file)
@@ -127,29 +121,25 @@ class LibraryType(PropertyGroup):
dst = Path(destination) dst = Path(destination)
if not src.exists(): if not src.exists():
print(f"Cannot copy file {src}: file not exist") print(f'Cannot copy file {src}: file not exist')
return return
dst.parent.mkdir(exist_ok=True, parents=True) dst.parent.mkdir(exist_ok=True, parents=True)
if src == dst: if src == dst:
print(f"Cannot copy file {src}: source and destination are the same") print(f'Cannot copy file {src}: source and destination are the same')
return return
print(f"Copy file from {src} to {dst}") print(f'Copy file from {src} to {dst}')
shutil.copy2(str(src), str(dst)) shutil.copy2(str(src), str(dst))
def load_datablocks( def load_datablocks(self, src, names=None, type='objects', link=True, expr=None, assets_only=False):
self, src, names=None, type="objects", link=True, expr=None, assets_only=False
):
"""Link or append a datablock from a blendfile""" """Link or append a datablock from a blendfile"""
if type.isupper(): if type.isupper():
type = f"{type.lower()}s" type = f'{type.lower()}s'
return load_datablocks( return load_datablocks(src, names=names, type=type, link=link, expr=expr, assets_only=assets_only)
src, names=names, type=type, link=link, expr=expr, assets_only=assets_only
)
def get_asset_data(self, asset): def get_asset_data(self, asset):
"""Extract asset information on a datablock""" """Extract asset information on a datablock"""
@@ -164,9 +154,9 @@ class LibraryType(PropertyGroup):
) )
def get_asset_relative_path(self, name, catalog): def get_asset_relative_path(self, name, catalog):
"""Get a relative path for the asset""" '''Get a relative path for the asset'''
name = self.norm_file_name(name) name = self.norm_file_name(name)
return Path(catalog, name, name).with_suffix(".blend") return Path(catalog, name, name).with_suffix('.blend')
def get_active_asset_library(self): def get_active_asset_library(self):
prefs = get_addon_prefs() prefs = get_addon_prefs()
@@ -176,8 +166,8 @@ class LibraryType(PropertyGroup):
return self return self
lib = None lib = None
if ".library_id" in asset_handle.asset_data: if '.library_id' in asset_handle.asset_data:
lib_id = asset_handle.asset_data[".library_id"] lib_id = asset_handle.asset_data['.library_id']
lib = next((l for l in prefs.libraries if l.id == lib_id), None) lib = next((l for l in prefs.libraries if l.id == lib_id), None)
if not lib: if not lib:
@@ -189,15 +179,15 @@ class LibraryType(PropertyGroup):
return lib return lib
def get_active_asset_path(self): def get_active_asset_path(self):
"""Get the full path of the active asset_handle from the asset brower""" '''Get the full path of the active asset_handle from the asset brower'''
prefs = get_addon_prefs() prefs = get_addon_prefs()
asset_handle = bpy.context.asset_file_handle asset_handle = bpy.context.asset_file_handle
lib = self.get_active_asset_library() lib = self.get_active_asset_library()
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
@@ -206,30 +196,30 @@ 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"""
return { return {
"asset_name": data["name"], 'asset_name': data['name'],
"asset_path": Path(data["filepath"]), 'asset_path': Path(data['filepath']),
"catalog": data["catalog"], 'catalog': data['catalog'],
"catalog_name": data["catalog"].replace("/", "_"), 'catalog_name': data['catalog'].replace('/', '_'),
} }
def format_path(self, template, data={}, **kargs): def format_path(self, template, data={}, **kargs):
@@ -241,8 +231,8 @@ class LibraryType(PropertyGroup):
else: else:
data = kargs data = kargs
if template.startswith("."): # the template is relative if template.startswith('.'): #the template is relative
template = Path(data["asset_path"], template).as_posix() template = Path(data['asset_path'], template).as_posix()
params = dict( params = dict(
**data, **data,
@@ -272,7 +262,11 @@ class LibraryType(PropertyGroup):
Path(asset_path).parent.mkdir(exist_ok=True, parents=True) Path(asset_path).parent.mkdir(exist_ok=True, parents=True)
bpy.data.libraries.write( bpy.data.libraries.write(
str(asset_path), {asset}, path_remap="NONE", fake_user=True, compress=True str(asset_path),
{asset},
path_remap="NONE",
fake_user=True,
compress=True
) )
# def read_catalog(self, directory=None): # def read_catalog(self, directory=None):
@@ -328,8 +322,8 @@ 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)
prop_value = Path(os.path.expandvars(prop_value)).resolve() prop_value = Path(os.path.expandvars(prop_value)).resolve()
@@ -339,15 +333,15 @@ class LibraryType(PropertyGroup):
return field_prop + rel_path return field_prop + rel_path
def format_from_ext(self, ext): def format_from_ext(self, ext):
if ext.startswith("."): if ext.startswith('.'):
ext = ext[1:] ext = ext[1:]
file_format = ext.upper() file_format = ext.upper()
if file_format == "JPG": if file_format == 'JPG':
file_format = "JPEG" file_format = 'JPEG'
elif file_format == "EXR": elif file_format == 'EXR':
file_format = "OPEN_EXR" file_format = 'OPEN_EXR'
return file_format return file_format
@@ -380,17 +374,12 @@ class LibraryType(PropertyGroup):
px = [0] * img_size[0] * img_size[1] * 4 px = [0] * img_size[0] * img_size[1] * 4
preview.image_pixels_float.foreach_get(px) preview.image_pixels_float.foreach_get(px)
img = bpy.data.images.new( img = bpy.data.images.new(name=filepath.name, width=img_size[0], height=img_size[1], is_data=True, alpha=True)
name=filepath.name,
width=img_size[0],
height=img_size[1],
is_data=True,
alpha=True,
)
img.pixels.foreach_set(px) img.pixels.foreach_set(px)
self.save_image(img, filepath, remove=True) self.save_image(img, filepath, remove=True)
def draw_header(self, layout): def draw_header(self, layout):
"""Draw the header of the Asset Browser Window""" """Draw the header of the Asset Browser Window"""
#layout.separator() #layout.separator()
@@ -402,27 +391,25 @@ class LibraryType(PropertyGroup):
self.module_type.gui.draw_context_menu(layout) self.module_type.gui.draw_context_menu(layout)
def generate_blend_preview(self, asset_info): def generate_blend_preview(self, asset_info):
asset_name = asset_info["name"] asset_name = asset_info['name']
catalog = asset_info["catalog"] catalog = asset_info['catalog']
asset_path = self.format_path(asset_info["filepath"]) asset_path = self.format_path(asset_info['filepath'])
dst_image_path = self.get_image_path(asset_name, asset_path, catalog) dst_image_path = self.get_image_path(asset_name, asset_path, catalog)
if dst_image_path.exists(): if dst_image_path.exists():
return return
# Check if a source image exists and if so copying it in the new directory # Check if a source image exists and if so copying it in the new directory
src_image_path = asset_info.get("image") src_image_path = asset_info.get('image')
if src_image_path: if src_image_path:
src_image_path = self.get_template_path( src_image_path = self.get_template_path(src_image_path, asset_name, asset_path, catalog)
src_image_path, asset_name, asset_path, catalog
)
if src_image_path and src_image_path.exists(): if src_image_path and src_image_path.exists():
self.copy_file(src_image_path, dst_image_path) self.copy_file(src_image_path, dst_image_path)
return return
print(f"Thumbnailing {asset_path} to {dst_image_path}") print(f'Thumbnailing {asset_path} to {dst_image_path}')
blender_thumbnailer = Path(bpy.app.binary_path).parent / "blender-thumbnailer" blender_thumbnailer = Path(bpy.app.binary_path).parent / 'blender-thumbnailer'
dst_image_path.parent.mkdir(exist_ok=True, parents=True) dst_image_path.parent.mkdir(exist_ok=True, parents=True)
@@ -431,7 +418,7 @@ class LibraryType(PropertyGroup):
success = dst_image_path.exists() success = dst_image_path.exists()
if not success: if not success:
empty_preview = RESOURCES_DIR / "empty_preview.png" empty_preview = RESOURCES_DIR / 'empty_preview.png'
self.copy_file(str(empty_preview), str(dst_image_path)) self.copy_file(str(empty_preview), str(dst_image_path))
return success return success
@@ -546,12 +533,14 @@ class LibraryType(PropertyGroup):
# def set_asset_catalog(self, asset, asset_data, catalog_data): # def set_asset_catalog(self, asset, asset_data, catalog_data):
# """Find the catalog if already exist or create it""" # """Find the catalog if already exist or create it"""
# catalog_name = asset_data['catalog'] # catalog_name = asset_data['catalog']
# catalog = catalog_data.get(catalog_name) # catalog = catalog_data.get(catalog_name)
# catalog_item = self.catalog.add(asset_data['catalog']) # catalog_item = self.catalog.add(asset_data['catalog'])
# asset.asset_data.catalog_id = catalog_item.id # asset.asset_data.catalog_id = catalog_item.id
# if not catalog: # if not catalog:
# catalog = {'id': str(uuid.uuid4()), 'name': catalog_name} # catalog = {'id': str(uuid.uuid4()), 'name': catalog_name}
# catalog_data[catalog_name] = catalog # catalog_data[catalog_name] = catalog
@@ -584,20 +573,16 @@ class LibraryType(PropertyGroup):
blend_name = asset_cache.norm_name blend_name = asset_cache.norm_name
path_parts = catalog_parts[:self.library.blend_depth] path_parts = catalog_parts[:self.library.blend_depth]
return Path( return Path(self.bundle_directory, *path_parts, blend_name, blend_name).with_suffix('.blend')
self.bundle_directory, *path_parts, blend_name, blend_name
).with_suffix(".blend")
def bundle(self, cache_diff=None): def bundle(self, cache_diff=None):
"""Group all new assets in one or multiple blends for the asset browser""" """Group all new assets in one or multiple blends for the asset browser"""
supported_types = ("FILE", "ACTION", "COLLECTION") supported_types = ('FILE', 'ACTION', 'COLLECTION')
supported_operations = ("ADD", "REMOVE", "MODIFY") supported_operations = ('ADD', 'REMOVE', 'MODIFY')
if self.data_type not in supported_types: if self.data_type not in supported_types:
print( print(f'{self.data_type} is not supported yet supported types are {supported_types}')
f"{self.data_type} is not supported yet supported types are {supported_types}"
)
return return
catalog = self.read_catalog() catalog = self.read_catalog()
@@ -611,64 +596,55 @@ 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( bpy.ops.assetlibrary.generate_previews(name=self.library.name, cache=str(tmp_cache_file))
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 = LibraryCacheDiff(cache_diff).read()#json.loads(Path(cache_diff).read_text(encoding='utf-8'))
cache_diff
).read() # json.loads(Path(cache_diff).read_text(encoding='utf-8'))
total_diffs = len(cache_diff) total_diffs = len(cache_diff)
print(f"Total Diffs={total_diffs}") print(f'Total Diffs={total_diffs}')
if total_diffs == 0: if total_diffs == 0:
print("No assets found") print('No assets found')
return return
i = 0 i = 0
for bundle_path, asset_diffs in cache_diff.group_by(self.get_asset_bundle_path): for bundle_path, asset_diffs in cache_diff.group_by(self.get_asset_bundle_path):
if bundle_path.exists(): if bundle_path.exists():
print(f"Opening existing bundle blend: {bundle_path}") print(f'Opening existing bundle blend: {bundle_path}')
bpy.ops.wm.open_mainfile(filepath=str(bundle_path)) bpy.ops.wm.open_mainfile(filepath=str(bundle_path))
else: else:
print(f"Create new bundle blend to: {bundle_path}") print(f'Create new bundle blend to: {bundle_path}')
bpy.ops.wm.read_homefile(use_empty=True) bpy.ops.wm.read_homefile(use_empty=True)
for asset_diff in asset_diffs: for asset_diff in asset_diffs:
if total_diffs <= 100 or i % int(total_diffs / 10) == 0: if total_diffs <= 100 or i % int(total_diffs / 10) == 0:
print(f"Progress: {int(i / total_diffs * 100)+1}") print(f'Progress: {int(i / total_diffs * 100)+1}')
operation = asset_diff.operation operation = asset_diff.operation
asset_cache = asset_diff.asset_cache asset_cache = asset_diff.asset_cache
asset = getattr(bpy.data, self.data_types).get(asset_cache.name) asset = getattr(bpy.data, self.data_types).get(asset_cache.name)
if operation == "REMOVE": if operation == 'REMOVE':
if asset: if asset:
getattr(bpy.data, self.data_types).remove(asset) getattr(bpy.data, self.data_types).remove(asset)
else: else:
print( print(f'ERROR : Remove Asset: {asset_cache.name} not found in {bundle_path}')
f"ERROR : Remove Asset: {asset_cache.name} not found in {bundle_path}"
)
continue continue
elif operation == "MODIFY": elif operation == 'MODIFY':
if not asset: if not asset:
print( print(f'WARNING: Modifiy Asset: {asset_cache.name} not found in {bundle_path} it will be created')
f"WARNING: Modifiy Asset: {asset_cache.name} not found in {bundle_path} it will be created"
)
if operation == "ADD" or not asset: if operation == 'ADD' or not asset:
if asset: if asset:
#raise Exception(f"Asset {asset_data['name']} Already in Blend") #raise Exception(f"Asset {asset_data['name']} Already in Blend")
print(f"Asset {asset_cache.name} Already in Blend") print(f"Asset {asset_cache.name} Already in Blend")
getattr(bpy.data, self.data_types).remove(asset) getattr(bpy.data, self.data_types).remove(asset)
#print(f"INFO: Add new asset: {asset_data['name']}") #print(f"INFO: Add new asset: {asset_data['name']}")
asset = getattr(bpy.data, self.data_types).new( asset = getattr(bpy.data, self.data_types).new(name=asset_cache.name)
name=asset_cache.name
)
asset.asset_mark() asset.asset_mark()
@@ -687,11 +663,12 @@ class LibraryType(PropertyGroup):
self.set_asset_tags(asset, asset_cache) self.set_asset_tags(asset, asset_cache)
self.set_asset_info(asset, asset_cache) self.set_asset_info(asset, asset_cache)
i += 1 i += 1
#self.write_asset_preview_file() #self.write_asset_preview_file()
print(f"Saving Blend to {bundle_path}") print(f'Saving Blend to {bundle_path}')
bundle_path.parent.mkdir(exist_ok=True, parents=True) bundle_path.parent.mkdir(exist_ok=True, parents=True)
bpy.ops.wm.save_as_mainfile(filepath=str(bundle_path), compress=True) bpy.ops.wm.save_as_mainfile(filepath=str(bundle_path), compress=True)
@@ -702,6 +679,7 @@ class LibraryType(PropertyGroup):
#self.write_catalog(catalog_data) #self.write_catalog(catalog_data)
catalog.write() catalog.write()
bpy.ops.wm.quit_blender() bpy.ops.wm.quit_blender()
# def unflatten_cache(self, cache): # def unflatten_cache(self, cache):
@@ -787,8 +765,9 @@ 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():
layout.prop(self, k, text=bpy.path.display_name(k)) layout.prop(self, k, text=bpy.path.display_name(k))
@@ -1,49 +1,44 @@
""" """
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 asset_library.plugins.library_plugin import LibraryPlugin
from asset_library.core.template import Template
from asset_library.core.file_utils import install_module
REQ_HEADERS = requests.utils.default_headers() REQ_HEADERS = requests.utils.default_headers()
REQ_HEADERS.update({"User-Agent": "Blender: PH Assets"}) REQ_HEADERS.update({"User-Agent": "Blender: PH Assets"})
class PolyHaven(LibraryPlugin):
class PolyHaven(LibraryType):
name = "Poly Haven" name = "Poly Haven"
# template_name : StringProperty() # template_name : StringProperty()
# template_file : StringProperty() # template_file : StringProperty()
directory: StringProperty(subtype="DIR_PATH") directory : StringProperty(subtype='DIR_PATH')
asset_type: EnumProperty( asset_type : EnumProperty(items=[(i.replace(' ', '_').upper(), i, '') for i in ('HDRIs', 'Models', 'Textures')], default='HDRIS')
items=[
(i.replace(" ", "_").upper(), i, "")
for i in ("HDRIs", "Models", "Textures")
],
default="HDRIS",
)
main_category : StringProperty( main_category : StringProperty(
default="artificial light, natural light, nature, studio, skies, urban" default='artificial light, natural light, nature, studio, skies, urban'
) )
secondary_category : StringProperty( secondary_category : StringProperty(
default="high constrast, low constrast, medium constrast, midday, morning-afternoon, night, sunrise-sunset" default='high constrast, low constrast, medium constrast, midday, morning-afternoon, night, sunrise-sunset'
) )
#blend_depth: IntProperty(default=1) #blend_depth: IntProperty(default=1)
@@ -69,35 +64,32 @@ class PolyHaven(LibraryType):
def format_asset_info(self, asset_info, asset_path): def format_asset_info(self, asset_info, asset_path):
# prend un asset info et output un asset description # prend un asset info et output un asset description
asset_path = self.prop_rel_path(asset_path, "source_directory") asset_path = self.prop_rel_path(asset_path, 'source_directory')
modified = asset_info.get("modified", time.time_ns()) modified = asset_info.get('modified', time.time_ns())
return dict( return dict(
filepath=asset_path, filepath=asset_path,
modified=modified, modified=modified,
library_id=self.library.id, library_id=self.library.id,
assets=[ assets=[dict(
dict( catalog=asset_data.get('catalog', asset_info['catalog']),
catalog=asset_data.get("catalog", asset_info["catalog"]), author=asset_data.get('author'),
author=asset_data.get("author"), metadata=asset_data.get('metadata', {}),
metadata=asset_data.get("metadata", {}), description=asset_data.get('description', ''),
description=asset_data.get("description", ""), tags=asset_data.get('tags', []),
tags=asset_data.get("tags", []),
type=self.data_type, type=self.data_type,
image=self.template_image, image=self.template_image,
video=self.template_video, video=self.template_video,
name=asset_data["name"], name=asset_data['name']) for asset_data in asset_info['assets']
) ]
for asset_data in asset_info["assets"]
],
) )
def fetch(self): def fetch(self):
"""Gather in a list all assets found in the folder""" """Gather in a list all assets found in the folder"""
print(f"Fetch Assets for {self.library.name}") print(f'Fetch Assets for {self.library.name}')
print("self.asset_type: ", self.asset_type) print('self.asset_type: ', self.asset_type)
url = f"https://api.polyhaven.com/assets?t={self.asset_type.lower()}" url = f"https://api.polyhaven.com/assets?t={self.asset_type.lower()}"
# url2 = f"https://polyhaven.com/{self.asset_type.lower()}" # url2 = f"https://polyhaven.com/{self.asset_type.lower()}"
# url += "&future=true" if early_access else "" # url += "&future=true" if early_access else ""
@@ -123,26 +115,27 @@ class PolyHaven(LibraryType):
for asset_info in res.json().values(): for asset_info in res.json().values():
main_category = None main_category = None
secondary_category = None secondary_category = None
for category in asset_info["categories"]: for category in asset_info['categories']:
if category in self.main_category and not main_category: if category in self.main_category and not main_category:
main_category = category main_category = category
if category in self.secondary_category and not secondary_category: if category in self.secondary_category and not secondary_category:
secondary_category = category secondary_category = category
if main_category and secondary_category: if main_category and secondary_category:
catalog = f"{main_category}_{secondary_category}" catalog = f'{main_category}_{secondary_category}'
if not catalog: if not catalog:
return return
asset_path = self.get_asset_path(asset_info["name"], catalog) asset_path = self.get_asset_path(asset_info['name'], catalog)
print("asset_path: ", asset_path) print('asset_path: ', asset_path)
asset_info = self.format_asset_info(asset_info, asset_path) asset_info = self.format_asset_info(asset_info, asset_path)
print("asset_info: ", asset_info) print('asset_info: ', asset_info)
# return self.format_asset_info([asset['name'], self.get_asset_path(asset['name'], catalog) for asset, asset_infos in res.json().items()]) # return self.format_asset_info([asset['name'], self.get_asset_path(asset['name'], catalog) for asset, asset_infos in res.json().items()])
# pp(res.json()) # pp(res.json())
# pp(res2.json()) # pp(res2.json())
# print(res2) # print(res2)
# return asset_infos # return asset_infos
@@ -1,27 +1,30 @@
""" """
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 asset_library.plugins.library_plugin import LibraryPlugin
from asset_library.core.bl_utils import load_datablocks
from asset_library.core.template import Template
class ScanFolder(LibraryType):
class ScanFolder(LibraryPlugin):
name = "Scan Folder" name = "Scan Folder"
source_directory: StringProperty(subtype="DIR_PATH") source_directory : StringProperty(subtype='DIR_PATH')
source_template_file : StringProperty() source_template_file : StringProperty()
source_template_image : StringProperty() source_template_image : StringProperty()
@@ -32,10 +35,10 @@ class ScanFolder(LibraryType):
layout.prop(self, "source_directory", text="Source: Directory") layout.prop(self, "source_directory", text="Source: Directory")
col = layout.column(align=True) col = layout.column(align=True)
col.prop(self, "source_template_file", icon="COPY_ID", text="Template file") col.prop(self, "source_template_file", icon='COPY_ID', text='Template file')
col.prop(self, "source_template_image", icon="COPY_ID", text="Template image") col.prop(self, "source_template_image", icon='COPY_ID', text='Template image')
col.prop(self, "source_template_video", icon="COPY_ID", text="Template video") col.prop(self, "source_template_video", icon='COPY_ID', text='Template video')
col.prop(self, "source_template_info", icon="COPY_ID", text="Template info") col.prop(self, "source_template_info", icon='COPY_ID', text='Template info')
def get_asset_path(self, name, catalog, directory=None): def get_asset_path(self, name, catalog, directory=None):
directory = directory or self.source_directory directory = directory or self.source_directory
@@ -47,26 +50,20 @@ class ScanFolder(LibraryType):
def get_image_path(self, name, catalog, filepath): def get_image_path(self, name, catalog, filepath):
catalog = self.norm_file_name(catalog) catalog = self.norm_file_name(catalog)
name = self.norm_file_name(name) name = self.norm_file_name(name)
return self.format_path( return self.format_path(self.source_template_image, dict(name=name, catalog=catalog, filepath=filepath))
self.source_template_image,
dict(name=name, catalog=catalog, filepath=filepath),
)
def get_video_path(self, name, catalog, filepath): def get_video_path(self, name, catalog, filepath):
catalog = self.norm_file_name(catalog) catalog = self.norm_file_name(catalog)
name = self.norm_file_name(name) name = self.norm_file_name(name)
return self.format_path( return self.format_path(self.source_template_video, dict(name=name, catalog=catalog, filepath=filepath))
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):
asset_path = self.prop_rel_path(asset_path, 'source_directory') asset_path = self.prop_rel_path(asset_path, 'source_directory')
@@ -101,10 +98,10 @@ class ScanFolder(LibraryType):
name=asset_data['name']) for asset_data in asset_datas name=asset_data['name']) for asset_data in asset_datas
] ]
) )
""" '''
def set_asset_preview(self, asset, asset_cache): def set_asset_preview(self, asset, asset_cache):
"""Load an externalize image as preview for an asset using the source template""" '''Load an externalize image as preview for an asset using the source template'''
asset_path = self.format_path(asset_cache.filepath) asset_path = self.format_path(asset_cache.filepath)
@@ -112,15 +109,15 @@ class ScanFolder(LibraryType):
if not image_template: if not image_template:
return return
image_path = self.find_path( image_path = self.find_path(image_template, asset_cache.to_dict(), filepath=asset_path)
image_template, asset_cache.to_dict(), filepath=asset_path
)
if image_path: if image_path:
with bpy.context.temp_override(id=asset): with bpy.context.temp_override(id=asset):
bpy.ops.ed.lib_id_load_custom_preview(filepath=str(image_path)) bpy.ops.ed.lib_id_load_custom_preview(
filepath=str(image_path)
)
else: else:
print(f"No image found for {image_template} on {asset.name}") print(f'No image found for {image_template} on {asset.name}')
if asset.preview: if asset.preview:
return asset.preview return asset.preview
@@ -128,8 +125,8 @@ class ScanFolder(LibraryType):
def bundle(self, cache_diff=None): def bundle(self, cache_diff=None):
"""Group all new assets in one or multiple blends for the asset browser""" """Group all new assets in one or multiple blends for the asset browser"""
if self.data_type not in ("FILE", "ACTION", "COLLECTION"): if self.data_type not in ('FILE', 'ACTION', 'COLLECTION'):
print(f"{self.data_type} is not supported yet") print(f'{self.data_type} is not supported yet')
return return
#catalog_data = self.read_catalog() #catalog_data = self.read_catalog()
@@ -144,58 +141,50 @@ 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( bpy.ops.assetlibrary.generate_previews(name=self.library.name, cache=str(tmp_cache_file))
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'))
if self.library.blend_depth == 0: if self.library.blend_depth == 0:
raise Exception("Blender depth must be 1 at min") raise Exception('Blender depth must be 1 at min')
total_assets = len(cache_diff) total_assets = len(cache_diff)
print(f"total_assets={total_assets}") print(f'total_assets={total_assets}')
if total_assets == 0: if total_assets == 0:
print("No assets found") print('No assets found')
return return
i = 0 i = 0
for blend_path, asset_cache_diffs in cache_diff.group_by( for blend_path, asset_cache_diffs in cache_diff.group_by(key=self.get_asset_bundle_path):
key=self.get_asset_bundle_path
):
if blend_path.exists(): if blend_path.exists():
print(f"Opening existing bundle blend: {blend_path}") print(f'Opening existing bundle blend: {blend_path}')
bpy.ops.wm.open_mainfile(filepath=str(blend_path)) bpy.ops.wm.open_mainfile(filepath=str(blend_path))
else: else:
print(f"Create new bundle blend to: {blend_path}") print(f'Create new bundle blend to: {blend_path}')
bpy.ops.wm.read_homefile(use_empty=True) bpy.ops.wm.read_homefile(use_empty=True)
for asset_cache_diff in asset_cache_diffs: for asset_cache_diff in asset_cache_diffs:
if total_assets <= 100 or i % int(total_assets / 10) == 0: if total_assets <= 100 or i % int(total_assets / 10) == 0:
print(f"Progress: {int(i / total_assets * 100)+1}") print(f'Progress: {int(i / total_assets * 100)+1}')
operation = asset_cache_diff.operation operation = asset_cache_diff.operation
asset_cache = asset_cache_diff.asset_cache asset_cache = asset_cache_diff.asset_cache
asset_name = asset_cache.name asset_name = asset_cache.name
asset = getattr(bpy.data, self.data_types).get(asset_name) asset = getattr(bpy.data, self.data_types).get(asset_name)
if operation == "REMOVE": if operation == 'REMOVE':
if asset: if asset:
getattr(bpy.data, self.data_types).remove(asset) getattr(bpy.data, self.data_types).remove(asset)
else: else:
print( print(f'ERROR : Remove Asset: {asset_name} not found in {blend_path}')
f"ERROR : Remove Asset: {asset_name} not found in {blend_path}"
)
continue continue
if asset_cache_diff.operation == "MODIFY" and not asset: if asset_cache_diff.operation == 'MODIFY' and not asset:
print( print(f'WARNING: Modifiy Asset: {asset_name} not found in {blend_path} it will be created')
f"WARNING: Modifiy Asset: {asset_name} not found in {blend_path} it will be created"
)
if operation == "ADD" or not asset: if operation == 'ADD' or not asset:
if asset: if asset:
#raise Exception(f"Asset {asset_name} Already in Blend") #raise Exception(f"Asset {asset_name} Already in Blend")
print(f"Asset {asset_name} Already in Blend") print(f"Asset {asset_name} Already in Blend")
@@ -204,9 +193,7 @@ class ScanFolder(LibraryType):
#print(f"INFO: Add new asset: {asset_name}") #print(f"INFO: Add new asset: {asset_name}")
asset = getattr(bpy.data, self.data_types).new(name=asset_name) asset = getattr(bpy.data, self.data_types).new(name=asset_name)
else: else:
print( print(f'operation {operation} not supported should be in (ADD, REMOVE, MODIFY)')
f"operation {operation} not supported should be in (ADD, REMOVE, MODIFY)"
)
continue continue
asset.asset_mark() asset.asset_mark()
@@ -219,11 +206,12 @@ class ScanFolder(LibraryType):
i += 1 i += 1
print(f"Saving Blend to {blend_path}") print(f'Saving Blend to {blend_path}')
blend_path.parent.mkdir(exist_ok=True, parents=True) blend_path.parent.mkdir(exist_ok=True, parents=True)
bpy.ops.wm.save_as_mainfile(filepath=str(blend_path), compress=True) bpy.ops.wm.save_as_mainfile(filepath=str(blend_path), compress=True)
# If the variable cache_diff was given we need to update the cache with the diff # If the variable cache_diff was given we need to update the cache with the diff
if cache is None: if cache is None:
cache = self.read_cache() cache = self.read_cache()
@@ -239,7 +227,7 @@ class ScanFolder(LibraryType):
def fetch(self): def fetch(self):
"""Gather in a list all assets found in the folder""" """Gather in a list all assets found in the folder"""
print(f"Fetch Assets for {self.library.name}") print(f'Fetch Assets for {self.library.name}')
source_directory = Path(self.source_directory) source_directory = Path(self.source_directory)
template_file = Template(self.source_template_file) template_file = Template(self.source_template_file)
@@ -250,23 +238,21 @@ class ScanFolder(LibraryType):
cache = self.read_cache() cache = self.read_cache()
print(f"Search for blend using glob template: {template_file.glob_pattern}") print(f'Search for blend using glob template: {template_file.glob_pattern}')
print(f"Scanning Folder {source_directory}...") print(f'Scanning Folder {source_directory}...')
#new_cache = LibraryCache() #new_cache = LibraryCache()
for asset_path in template_file.glob(source_directory): for asset_path in template_file.glob(source_directory):
source_rel_path = self.prop_rel_path(asset_path, "source_directory") source_rel_path = self.prop_rel_path(asset_path, 'source_directory')
modified = asset_path.stat().st_mtime_ns modified = asset_path.stat().st_mtime_ns
# Check if the asset description as already been cached # Check if the asset description as already been cached
file_cache = next((a for a in cache if a.filepath == source_rel_path), None) file_cache = next((a for a in cache if a.filepath == source_rel_path), None)
if file_cache: if file_cache:
if ( if file_cache.modified >= modified: #print(asset_path, 'is skipped because not modified')
file_cache.modified >= modified
): # print(asset_path, 'is skipped because not modified')
continue continue
else: else:
file_cache = cache.add(filepath=source_rel_path) file_cache = cache.add(filepath=source_rel_path)
@@ -275,16 +261,17 @@ class ScanFolder(LibraryType):
field_data = template_file.parse(rel_path) field_data = template_file.parse(rel_path)
# Create the catalog path from the actual path of the asset # Create the catalog path from the actual path of the asset
catalog = [ catalog = [v for k,v in sorted(field_data.items()) if re.findall('cat[0-9]+', k)]
v for k, v in sorted(field_data.items()) if re.findall("cat[0-9]+", k)
]
#catalogs = [c.replace('_', ' ').title() for c in catalogs] #catalogs = [c.replace('_', ' ').title() for c in catalogs]
asset_name = field_data.get("asset_name", asset_path.stem) asset_name = field_data.get('asset_name', asset_path.stem)
if self.data_type == "FILE": if self.data_type == 'FILE':
file_cache.set_data( file_cache.set_data(
name=asset_name, type="FILE", catalog=catalog, modified=modified name=asset_name,
type='FILE',
catalog=catalog,
modified=modified
) )
continue continue
@@ -296,11 +283,9 @@ class ScanFolder(LibraryType):
# continue # continue
# Scan the blend file for assets inside # Scan the blend file for assets inside
print(f"Scanning blendfile {asset_path}...") print(f'Scanning blendfile {asset_path}...')
assets = self.load_datablocks( assets = self.load_datablocks(asset_path, type=self.data_types, link=True, assets_only=True)
asset_path, type=self.data_types, link=True, assets_only=True print(f'Found {len(assets)} {self.data_types} inside')
)
print(f"Found {len(assets)} {self.data_types} inside")
for asset in assets: for asset in assets:
#catalog_path = catalog_ids.get(asset.asset_data.catalog_id) #catalog_path = catalog_ids.get(asset.asset_data.catalog_id)
@@ -314,3 +299,4 @@ class ScanFolder(LibraryType):
getattr(bpy.data, self.data_types).remove(asset) getattr(bpy.data, self.data_types).remove(asset)
return cache return cache
+29 -799
View File
@@ -1,829 +1,59 @@
import bpy import bpy
import os from bpy.types import AddonPreferences
from os.path import abspath, join from bpy.props import (CollectionProperty, StringProperty)
from bpy.types import AddonPreferences, PointerProperty, PropertyGroup from . properties import AssetLibrary
from bpy.props import ( from . core.bl_utils import get_addon_prefs
BoolProperty, from . core.lib_utils import update_library_path
StringProperty,
CollectionProperty,
EnumProperty,
IntProperty,
)
from asset_library.constants import (
DATA_TYPES,
DATA_TYPE_ITEMS,
ICONS,
RESOURCES_DIR,
LIBRARY_TYPE_DIR,
LIBRARY_TYPES,
ADAPTERS,
)
from asset_library.common.file_utils import import_module_from_path, norm_str
from asset_library.common.bl_utils import get_addon_prefs
from asset_library.common.library_cache import LibraryCache
from asset_library.common.catalog import Catalog
# from asset_library.common.functions import get_catalog_path
from pathlib import Path
import importlib
import inspect
def update_library_config(self, context):
print("update_library_config not yet implemented")
def update_library_path(self, context):
prefs = get_addon_prefs()
self["bundle_directory"] = str(self.library_path)
if not self.custom_bundle_name:
self["custom_bundle_name"] = self.name
if not self.custom_bundle_directory:
custom_bundle_dir = Path(prefs.bundle_directory, self.library_name).resolve()
self["custom_bundle_directory"] = str(custom_bundle_dir)
# if self.custom_bundle_directory:
# self['custom_bundle_directory'] = abspath(bpy.path.abspath(self.custom_bundle_directory))
# else:
# bundle_directory = join(prefs.bundle_directory, norm_str(self.name))
# self['custom_bundle_directory'] = abspath(bundle_directory)
self.set_library_path()
def update_all_library_path(self, context):
# print('update_all_assetlib_paths')
prefs = get_addon_prefs()
# if self.custom_bundle_directory:
# self['custom_bundle_directory'] = abspath(bpy.path.abspath(self.custom_bundle_directory))
for lib in prefs.libraries:
update_library_path(lib, context)
# lib.set_library_path()
def get_library_type_items(self, context):
# prefs = get_addon_prefs()
items = [("NONE", "None", "", 0)]
items += [
(norm_str(a.name, format=str.upper), a.name, "", i + 1)
for i, a in enumerate(LIBRARY_TYPES)
]
return items
def get_adapters_items(self, context):
# prefs = get_addon_prefs()
items = [("NONE", "None", "", 0)]
items += [
(norm_str(a.name, format=str.upper), a.name, "", i + 1)
for i, a in enumerate(ADAPTERS)
]
return items
def get_library_items(self, context):
prefs = get_addon_prefs()
items = [("NONE", "None", "", 0)]
items += [
(l.name, l.name, "", i + 1) for i, l in enumerate(prefs.libraries) if l != self
]
return items
def get_store_library_items(self, context):
# prefs = get_addon_prefs()
# libraries = [l for l in prefs.libraries if l.merge_library == self.name]
return [
(l.name, l.name, "", i) for i, l in enumerate([self] + self.merge_libraries)
]
class LibraryTypes(PropertyGroup):
def __iter__(self):
return (
getattr(self, p)
for p in self.bl_rna.properties.keys()
if p not in ("rna_type", "name")
)
class Adapters(PropertyGroup):
def __iter__(self):
return (
getattr(self, p)
for p in self.bl_rna.properties.keys()
if p not in ("rna_type", "name")
)
class AssetLibrary(PropertyGroup):
name: StringProperty(
name="Name", default="Action Library", update=update_library_path
)
id: StringProperty()
auto_bundle: BoolProperty(name="Auto Bundle", default=False)
expand: BoolProperty(name="Expand", default=False)
use: BoolProperty(name="Use", default=True, update=update_library_path)
data_type: EnumProperty(name="Type", items=DATA_TYPE_ITEMS, default="COLLECTION")
# template_image : StringProperty(default='', description='../{name}_image.png')
# template_video : StringProperty(default='', description='../{name}_video.mov')
# template_info : StringProperty(default='', description='../{name}_asset_info.json')
bundle_directory: StringProperty(
name="Bundle Directory", subtype="DIR_PATH", default=""
)
use_custom_bundle_directory: BoolProperty(default=False, update=update_library_path)
custom_bundle_directory: StringProperty(
name="Bundle Directory",
subtype="DIR_PATH",
default="",
update=update_library_path,
)
# use_merge : BoolProperty(default=False, update=update_library_path)
use_custom_bundle_name: BoolProperty(default=False, update=update_library_path)
custom_bundle_name: StringProperty(name="Merge Name", update=update_library_path)
# merge_library : EnumProperty(name='Merge Library', items=get_library_items, update=update_library_path)
# merge_name : StringProperty(name='Merge Name', update=update_library_path)
# Library when adding an asset to the library if merge with another
store_library: EnumProperty(items=get_store_library_items, name="Library")
template: StringProperty()
expand_extra: BoolProperty(name="Expand", default=False)
blend_depth: IntProperty(name="Blend Depth", default=1)
# source_directory : StringProperty(
# name="Path",
# subtype='DIR_PATH',
# default='',
# update=update_library_path
# )
# library_type : EnumProperty(items=library_type_ITEMS)
library_types: bpy.props.PointerProperty(type=LibraryTypes)
library_type_name: EnumProperty(items=get_library_type_items)
adapters: bpy.props.PointerProperty(type=Adapters)
adapter_name: EnumProperty(items=get_adapters_items)
parent_name: StringProperty()
# data_file_path : StringProperty(
# name="Path",
# subtype='FILE_PATH',
# default='',
# )
# def __init__(self):
# self.library_types.parent = self
@property
def parent(self):
prefs = get_addon_prefs()
if self.parent_name:
return prefs.libraries[self.parent_name]
@property
def merge_libraries(self):
prefs = get_addon_prefs()
return [
l
for l in prefs.libraries
if l != self and (l.library_path == self.library_path)
]
@property
def child_libraries(self):
prefs = get_addon_prefs()
return [l for l in prefs.libraries if l != self and (l.parent == self)]
@property
def data_types(self):
data_type = self.data_type
if data_type == "FILE":
data_type = "COLLECTION"
return f"{data_type.lower()}s"
@property
def library_type(self):
name = norm_str(self.library_type_name)
if not hasattr(self.library_types, name):
return
return getattr(self.library_types, name)
@property
def adapter(self):
name = norm_str(self.adapter_name)
if not hasattr(self.adapters, name):
return
return getattr(self.adapters, name)
@property
def library(self):
prefs = get_addon_prefs()
asset_lib_ref = bpy.context.space_data.params.asset_library_ref
# TODO work also outside asset_library_area
if asset_lib_ref not in prefs.libraries:
return None
return prefs.libraries[asset_lib_ref]
@property
def library_path(self):
prefs = get_addon_prefs()
library_name = self.library_name
# if not self.use_custom_bundle_name:
# library_name = norm_str(library_name)
if self.use_custom_bundle_directory:
return Path(self.custom_bundle_directory).resolve()
else:
library_name = norm_str(library_name)
return Path(prefs.bundle_directory, library_name).resolve()
@property
def bundle_dir(self):
return self.library_path.as_posix()
@property
def library_name(self):
if self.use_custom_bundle_name:
return self.custom_bundle_name
return self.name
def read_catalog(self):
return Catalog(self.library_path).read()
def read_cache(self, filepath=None):
if filepath:
return LibraryCache(filepath).read()
return LibraryCache.from_library(self).read()
def clear_library_path(self):
# print('Clear Library Path', self.name)
prefs = bpy.context.preferences
libs = prefs.filepaths.asset_libraries
# path = self.library_path.as_posix()
for l in reversed(libs):
# lib_path = Path(l.path).resolve().as_posix()
prev_name = self.get("asset_library") or self.library_name
# print(l.name, prev_name)
if l.name == prev_name:
index = list(libs).index(l)
try:
bpy.ops.preferences.asset_library_remove(index=index)
return
except AttributeError:
pass
# print('No library removed')
def set_dict(self, data, obj=None):
""" "Recursive method to set all attribute from a dict to this instance"""
if obj is None:
obj = self
# Make shure the input dict is not modidied
data = data.copy()
# print(obj)
for key, value in data.items():
if isinstance(value, dict):
if "name" in value:
setattr(obj, f"{key}_name", value.pop("name"))
# print('Nested value', getattr(obj, key))
self.set_dict(value, obj=getattr(obj, key))
elif key in obj.bl_rna.properties.keys():
if key == "id":
value = str(value)
elif key == "custom_bundle_name":
if not "use_custom_bundle_name" in data.values():
obj["use_custom_bundle_name"] = True
elif isinstance(value, str):
value = os.path.expandvars(value)
value = os.path.expanduser(value)
# print('set attr', key, value)
setattr(obj, key, value)
# obj[key] = value
else:
print(f"Prop {key} of {obj} not exist")
self["bundle_directory"] = str(self.library_path)
if not self.custom_bundle_name:
self["custom_bundle_name"] = self.name
# self.library_type_name = data['library_type']
# if not self.library_type:
# print(f"No library_type named {data['library_type']}")
# return
# for key, value in data.items():
# if key == 'options':
# for k, v in data['options'].items():
# setattr(self.library_type, k, v)
# elif key in self.bl_rna.properties.keys():
# if key == 'id':
# value = str(value)
# if key == 'custom_bundle_name':
# if not 'use_custom_bundle_name' in data.values():
# self["use_custom_bundle_name"] = True
# self[key] = value
def to_dict(self):
data = {
p: getattr(self, p)
for p in self.bl_rna.properties.keys()
if p != "rna_type"
}
if self.library_type:
data["library_type"] = self.library_type.to_dict()
data["library_type"]["name"] = data.pop("library_type_name")
del data["library_types"]
if self.adapter:
data["adapter"] = self.adapter.to_dict()
data["adapter"]["name"] = data.pop("adapter_name")
del data["adapters"]
return data
def set_library_path(self):
"""Update the Blender Preference Filepaths tab with the addon libraries"""
prefs = bpy.context.preferences
name = self.library_name
lib_path = self.library_path
self.clear_library_path()
if not self.use or not lib_path:
# if all(not l.use for l in self.merge_libraries):
# self.clear_library_path()
return
# lib = None
# if self.get('asset_library'):
# #print('old_name', self['asset_library'])
# lib = prefs.filepaths.asset_libraries.get(self['asset_library'])
# if not lib:
# #print('keys', prefs.filepaths.asset_libraries.keys())
# #print('name', name)
# #print(prefs.filepaths.asset_libraries.get(name))
# lib = prefs.filepaths.asset_libraries.get(name)
# Create the Asset Library Path
lib = prefs.filepaths.asset_libraries.get(name)
if not lib:
# print(f'Creating the lib {name}')
try:
bpy.ops.preferences.asset_library_add(directory=str(lib_path))
except AttributeError:
return
lib = prefs.filepaths.asset_libraries[-1]
lib.name = name
self["asset_library"] = name
lib.path = str(lib_path)
@property
def is_user(self):
prefs = get_addon_prefs()
return self in prefs.user_libraries.values()
@property
def is_env(self):
prefs = get_addon_prefs()
return self in prefs.env_libraries.values()
def add_row(
self, layout, data=None, prop=None, label="", boolean=None, factor=0.39
):
"""Act like the use_property_split but with more control"""
enabled = True
split = layout.split(factor=factor, align=True)
row = split.row(align=False)
row.use_property_split = False
row.alignment = "RIGHT"
row.label(text=str(label))
if boolean:
boolean_data = self
if isinstance(boolean, (list, tuple)):
boolean_data, boolean = boolean
row.prop(boolean_data, boolean, text="")
enabled = getattr(boolean_data, boolean)
row = split.row(align=True)
row.enabled = enabled
if isinstance(data, str):
row.label(text=data)
else:
row.prop(data or self, prop, text="")
return split
def draw_operators(self, layout):
row = layout.row(align=True)
row.alignment = "RIGHT"
row.prop(self, "library_type_name", text="")
row.prop(self, "auto_bundle", text="", icon="UV_SYNC_SELECT")
row.operator("assetlib.diff", text="", icon="FILE_REFRESH").name = self.name
op = row.operator("assetlib.bundle", icon="MOD_BUILD", text="")
op.name = self.name
layout.separator(factor=3)
def draw(self, layout):
prefs = get_addon_prefs()
# box = layout.box()
row = layout.row(align=True)
# row.use_property_split = False
# row.alignment = 'LEFT'
icon = "DISCLOSURE_TRI_DOWN" if self.expand else "DISCLOSURE_TRI_RIGHT"
row.prop(self, "expand", icon=icon, emboss=False, text="")
if self.is_user:
row.prop(self, "use", text="")
row.prop(self, "data_type", icon_only=True, emboss=False)
row.prop(self, "name", text="")
self.draw_operators(row)
index = list(prefs.user_libraries).index(self)
row.operator(
"assetlib.remove_user_library", icon="X", text="", emboss=False
).index = index
else:
row.prop(self, "use", text="")
row.label(icon=ICONS[self.data_type])
# row.label(text=self.name)
subrow = row.row(align=True)
subrow.alignment = "LEFT"
subrow.prop(self, "expand", emboss=False, text=self.name)
# row.separator_spacer()
self.draw_operators(row)
sub_row = row.row()
sub_row.enabled = False
sub_row.label(icon="FAKE_USER_ON")
if self.expand:
col = layout.column(align=False)
col.use_property_split = True
# row = col.row(align=True)
row = self.add_row(
col,
prop="custom_bundle_name",
boolean="use_custom_bundle_name",
label="Custom Bundle Name",
)
row.enabled = not self.use_custom_bundle_directory
prop = "bundle_directory"
if self.use_custom_bundle_directory:
prop = "custom_bundle_directory"
self.add_row(
col,
prop=prop,
boolean="use_custom_bundle_directory",
label="Custom Bundle Directory",
)
col.prop(self, "blend_depth")
# subcol = col.column(align=True)
# subcol.prop(self, "template_info", text='Template Info', icon='COPY_ID')
# subcol.prop(self, "template_image", text='Template Image', icon='COPY_ID')
# subcol.prop(self, "template_video", text='Template Video', icon='COPY_ID')
if self.library_type:
col.separator()
self.library_type.draw_prefs(col)
for lib in self.child_libraries:
lib.draw(layout)
col.separator()
class Collections:
"""Util Class to merge multiple collections"""
collections = []
def __init__(self, *collection):
self.collections = collection
for col in collection:
# print('Merge methods')
for attr in dir(col):
if attr.startswith("_"):
continue
value = getattr(col, attr)
# if not callable(value):
# continue
setattr(self, attr, value)
def __contains__(self, item):
if isinstance(item, str):
return item in self.to_dict()
else:
return item in self
def __iter__(self):
return self.to_list().__iter__()
def __getitem__(self, item):
if isinstance(item, int):
return self.to_list()[item]
else:
return self.to_dict()[item]
def get(self, item, fallback=None):
return self.to_dict().get(item) or fallback
def to_dict(self):
return {k: v for c in self.collections for k, v in c.items()}
def to_list(self):
return [v for c in self.collections for v in c.values()]
def get_parent(self, item):
for c in self.collections:
if item in c.values():
return c
def index(self, item):
c = self.get_parent(item)
if not c:
return item in self
return list(c.values()).index(item)
# class AssetLibraryOptions(PropertyGroup):
# pass
class AssetLibraryPrefs(AddonPreferences): class AssetLibraryPrefs(AddonPreferences):
bl_idname = __package__ bl_idname = __package__
adapters = [] config_path : StringProperty(subtype="FILE_PATH")
library_types = [] libraries : CollectionProperty(type=AssetLibrary)
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( bundle_directory : StringProperty(
name="Path", subtype="DIR_PATH", default="", update=update_all_library_path name="Path",
subtype='DIR_PATH',
default='',
update=lambda s, c: update_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): def draw(self, context):
prefs = get_addon_prefs() prefs = get_addon_prefs()
layout = self.layout layout = self.layout
# layout.use_property_split = True col = layout.column(align=False)
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.prop(self, "config_path", text='Config')
col.prop(self, "bundle_directory", text='Bundle Directory')
col.separator() col.separator()
col.prop(self, "library_type_directory") row = col.row()
col.prop(self, "config_directory") row.label(text='Libraries:')
#row.alignment = 'RIGHT'
row.separator_spacer()
row.operator("assetlibrary.reload_addon", icon='FILE_REFRESH', text='')
row.operator("assetlibrary.add_library", icon="ADD", text='', emboss=False)
col.separator() for lib in self.libraries:# list(self.env_libraries) + list(self.user_libraries):
lib.draw(col)
# 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, classes = (
# ConformAssetLibrary,
AssetLibrary,
AssetLibraryPrefs, AssetLibraryPrefs,
] )
def register(): def register():
for cls in classes: for cls in classes:
bpy.utils.register_class(cls) 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(): def unregister():
for cls in reversed(classes + LIBRARY_TYPES): for cls in reversed(classes):
bpy.utils.unregister_class(cls) bpy.utils.unregister_class(cls)
LIBRARY_TYPES.clear()
+186
View File
@@ -0,0 +1,186 @@
import inspect
import os
import bpy
from bpy.types import (AddonPreferences, PropertyGroup)
from bpy.props import (BoolProperty, StringProperty, CollectionProperty,
EnumProperty, IntProperty, PointerProperty)
from .constants import PLUGINS, PLUGINS_DIR, PLUGINS_ITEMS
from .core.file_utils import import_module_from_path, norm_str
from .core.bl_utils import get_addon_prefs
from .core.lib_utils import update_library_path
def load_plugins():
from .plugins.library_plugin import LibraryPlugin
print('Asset Library: Load Library Plugins')
plugin_files = list(PLUGINS_DIR.glob('*.py'))
# if self.plugin_directory:
# user_plugin_DIR = Path(self.plugin_directory)
# if user_plugin_DIR.exists():
# plugin_files += list(user_plugin_DIR.glob('*.py'))
for plugin_file in plugin_files:
if plugin_file.stem.startswith('_'):
continue
mod = import_module_from_path(plugin_file)
for name, obj in inspect.getmembers(mod):
if not inspect.isclass(obj) or (obj is LibraryPlugin):
continue
if (LibraryPlugin not in obj.__mro__) or (obj in PLUGINS):
continue
try:
print(f'Register Plugin {name}')
bpy.utils.register_class(obj)
setattr(Plugins, norm_str(obj.name), PointerProperty(type=obj))
PLUGINS[obj.name] = obj
except Exception as e:
print(f'Could not register plugin {name}')
print(e)
plugins = sorted(PLUGINS.keys())
plugin_items = [('none', 'None', '', 0)]
plugin_items += [(norm_str(p), p, "") for p in plugins]
PLUGINS_ITEMS[:] = plugin_items
return PLUGINS
class Plugins(PropertyGroup):
"""Container holding the registed library plugins"""
def __iter__(self):
return (getattr(self, p) for p in self.bl_rna.properties.keys() if p not in ('rna_type', 'name'))
class AssetLibrary(PropertyGroup):
"""Library item defining one library with his plugin and settings"""
name : StringProperty(name='Name', default='', update=lambda s, c : update_library_path())
expand : BoolProperty(name='Expand', default=False)
use : BoolProperty(name='Use', default=True, update=lambda s, c : update_library_path())
is_user : BoolProperty(default=True)
path : StringProperty(subtype='DIR_PATH', update=lambda s, c : update_library_path())
plugins : PointerProperty(type=Plugins)
plugin_name : EnumProperty(items=lambda s, c : PLUGINS_ITEMS)
@property
def plugin(self):
return getattr(self.plugins, self.plugin_name, None)
@property
def index(self):
prefs = get_addon_prefs()
return list(prefs.libraries).index(self)
"""
def draw_operators(self, layout):
row = layout.row(align=True)
row.alignment = 'RIGHT'
row.prop(self, 'plugin_name', text='')
row.prop(self, 'auto_bundle', text='', icon='UV_SYNC_SELECT')
row.operator("assetlibrary.diff", text='', icon='FILE_REFRESH').name = self.name
op = row.operator("assetlibrary.sync", icon='MOD_BUILD', text='')
op.name = self.name
layout.separator(factor=3)
"""
def set_dict(self, data, obj=None):
""""Recursive method to set all attribute from a dict to this instance"""
if obj is None:
obj = self
# Make shure the input dict is not modidied
data = data.copy()
for key, value in data.items():
if isinstance(value, dict):
self.set_dict(value, obj=getattr(obj, key))
elif key in obj.bl_rna.properties.keys():
if isinstance(value, str):
value = os.path.expandvars(value)
value = os.path.expanduser(value)
setattr(obj, key, value)
else:
print(f'Prop {key} of {obj} not exist')
def draw(self, layout):
prefs = get_addon_prefs()
#col = layout.column(align=True)
box = layout.box()
row = box.row(align=True)
icon = "DISCLOSURE_TRI_DOWN" if self.expand else "DISCLOSURE_TRI_RIGHT"
row.prop(self, 'expand', icon=icon, emboss=False, text='')
row.prop(self, 'use', text='')
#row.label(icon="ASSET_MANAGER")
row.prop(self, 'name', text='')
row.separator(factor=0.5)
sub = row.row()
sub.alignment = 'RIGHT'
sub.prop(self, 'plugin_name', text='')
row.separator(factor=0.5)
op = row.operator("assetlibrary.synchronize", icon='UV_SYNC_SELECT', text='')
op.name = self.name
row.separator(factor=0.5)
row.operator("assetlibrary.remove_library", icon="REMOVE", text='', emboss=False).index = self.index
#self.draw_operators(row)
if self.expand:
col = box.column(align=False)
col.use_property_split = True
col.prop(self, 'path', text='Path')
if self.plugin:
col.separator()
self.plugin.draw_prefs(col)
class WindowManagerProperties(PropertyGroup):
"""Library item defining one library with his plugin and settings"""
asset : PointerProperty(type=bpy.types.ID)
classes = (
Plugins,
AssetLibrary,
WindowManagerProperties
)
def register():
for cls in classes:
bpy.utils.register_class(cls)
bpy.types.WindowManager.asset_library = PointerProperty(type=WindowManagerProperties)
load_plugins()
def unregister():
for cls in reversed(classes):
bpy.utils.unregister_class(cls)
del bpy.types.WindowManager.asset_library
Binary file not shown.
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 731 B

+42
View File
@@ -0,0 +1,42 @@
import argparse
import sys
import json
from pathlib import Path
import bpy
from asset_library import constants
from asset_library.core.bl_utils import load_datablocks
def publish_asset(data_type, name):
bpy.app.use_userpref_skip_save_on_exit = True
blend_file = bpy.data.filepath
preview_blend = constants.RESOURCES_DIR / 'asset_preview.blend'
col = load_datablocks(preview_blend, names='Preview', type='collections', link=False)
bpy.context.scene.collection.children.link(col)
if data_type == 'node_groups':
ntree = bpy.data.node_groups[name]
mod = bpy.data.objects['Cube'].modifiers.new(ntree_name, 'NODES')
mod.node_group = ntree
bpy.context.preferences.filepaths.save_version = 0
bpy.ops.wm.save_mainfile(compress=True, exit=True)
#bpy.ops.wm.quit_blender()
if __name__ == '__main__' :
parser = argparse.ArgumentParser(description='build_collection_blends',
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument('--data-type')
parser.add_argument('--datablock')
if '--' in sys.argv :
index = sys.argv.index('--')
sys.argv = [sys.argv[index-1], *sys.argv[index+1:]]
args = parser.parse_args()
publish_asset(**vars(args))
+96
View File
@@ -0,0 +1,96 @@
import argparse
import sys
import json
from pathlib import Path
import bpy
from datetime import datetime
from asset_library import constants
from asset_library.core.bl_utils import load_datablocks
from asset_library.core.lib_utils import clear_time_tag, create_time_tag, version_file
from asset_library.core.catalog import Catalog
def get_all_datablocks():
blend_datas = [
bpy.data.collections, bpy.data.objects,
bpy.data.materials, bpy.data.node_groups, bpy.data.worlds]
return [o for blend_data in blend_datas for o in blend_data]
def publish_library_assets(library, assets, catalogs):
bpy.app.use_userpref_skip_save_on_exit = True
bpy.context.preferences.filepaths.save_version = 0
for asset, catalog_path in zip(assets, catalogs):
asset_path, asset_type, asset_name = asset.rsplit('/', 2)
#print(asset_path, asset_type, asset_name)
asset = load_datablocks(asset_path, names=asset_name, type=asset_type, link=False)
if not asset.asset_data:
asset.asset_mark()
# clear asset_mark of all other assets
for datablock in get_all_datablocks():
if datablock != asset and datablock.asset_data:
datablock.asset_clear()
if asset_type == 'node_groups':
mod = bpy.data.objects['Cube'].modifiers.new(asset.name, 'NODES')
mod.node_group = asset
elif asset_type == 'objects':
bpy.data.objects.remove(bpy.data.objects['Cube'])
bpy.data.collections['Preview'].objects.link(asset)
elif asset_type == 'materials':
bpy.data.objects['Cube'].data.materials.append(asset)
#catalog_path = asset.asset_data.catalog_simple_name.replace('-', '/')
asset_publish_path = Path(library, catalog_path, asset.name).with_suffix('.blend')
asset_publish_path.parent.mkdir(parents=True, exist_ok=True)
# Assign or Create catalog
catalog = Catalog(library)
catalog.read()
if catalog_item := catalog.get(path=catalog_path):
catalog_id = catalog_item.id
else:
catalog_id = catalog.add(catalog_path).id
catalog.write()
asset.asset_data.catalog_id = catalog_id
# Created time tag
clear_time_tag(asset)
create_time_tag(asset)
version_file(asset_publish_path)
bpy.ops.object.make_local(type='ALL')
bpy.ops.file.make_paths_relative()
bpy.ops.wm.save_as_mainfile(filepath=str(asset_publish_path), compress=True, copy=True, relative_remap=True)
bpy.ops.wm.revert_mainfile()
bpy.ops.wm.quit_blender()
if __name__ == '__main__' :
parser = argparse.ArgumentParser(description='build_collection_blends',
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument('--library')
parser.add_argument('--assets', nargs='+')
parser.add_argument('--catalogs', nargs='+')
if '--' in sys.argv :
index = sys.argv.index('--')
sys.argv = [sys.argv[index-1], *sys.argv[index+1:]]
args = parser.parse_args()
publish_library_assets(**vars(args))
+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))
-286
View File
@@ -1,286 +0,0 @@
import bpy
from pathlib import Path
from asset_library.common.file_utils import read_file, write_file
from copy import deepcopy
import time
from itertools import groupby
class AssetCache:
def __init__(self, file_cache, data):
self.file_cache = file_cache
self._data = data
self.catalog = data["catalog"]
self.author = data.get("author", "")
self.description = data.get("description", "")
self.tags = data.get("tags", [])
self.type = data.get("type")
self.name = data["name"]
self._metadata = data.get("metadata", {})
@property
def filepath(self):
return self.file_cache.filepath
@property
def metadata(self):
metadata = {".library_id": self.library.id, ".filepath": self.filepath}
metadata.update(self.metadata)
return metadata
@property
def norm_name(self):
return self.name.replace(" ", "_").lower()
def to_dict(self):
return dict(
catalog=self.catalog,
author=self.author,
metadata=self.metadata,
description=self.description,
tags=self.tags,
type=self.type,
name=self.name,
)
def __str__(self):
return f"AssetCache(name={self.name}, type={self.type}, catalog={self.catalog})"
class FileCache:
def __init__(self, library_cache, data):
self.library_cache = library_cache
self.filepath = data["filepath"]
self.modified = data.get("modified", time.time_ns())
self._data = []
for asset_cache_data in data.get("assets", []):
self.add(asset_cache_data)
def add(self, asset_cache_data):
asset_cache = AssetCache(self, asset_cache_data)
self._data.append(asset_cache)
def to_dict(self):
return dict(
filepath=self.filepath.as_posix(),
modified=self.modified,
library_id=self.library_cache.library.id,
assets=[asset_cache.to_dict() for asset_cache in self],
)
def __iter__(self):
return self._data.__iter__()
def __getitem__(self, key):
return self._data[key]
def __str__(self):
return f"FileCache(filepath={self.filepath})"
class AssetCacheDiff:
def __init__(self, library_diff, asset_cache, operation):
self.library_cache = library_cache
self.filepath = data["filepath"]
self.operation = operation
class LibraryCacheDiff:
def __init__(self, filepath=None):
self.filepath = filepath
self._data = []
def add(self, asset_diff):
asset_diff = AssetCacheDiff(self, asset_diff)
self._data.append(asset_cache_diff)
def set(self, asset_diffs):
for asset_diff in asset_diffs:
self.add(asset_diff)
def read(self):
print(f"Read cache from {self.filepath}")
for asset_diff_data in read_file(self.filepath):
self.add(asset_diff_data)
return self
def group_by(self, key):
"""Return groups of file cache diff using the key provided"""
data = list(self).sort(key=key)
return groupby(data, key=key)
def __iter__(self):
return iter(self._data)
def __len__(self):
return len(self._data)
def __getitem__(self, key):
return self._data[key]
class LibraryCache:
def __init__(self, directory, id):
self.directory = directory
self.id = id
self._data = []
@classmethod
def from_library(cls, library):
return cls(library.library_path, library.id)
@property
def filename(self):
return f"blender_assets.{self.id}.json"
@property
def filepath(self):
"""Get the filepath of the library json file relative to the library"""
return self.directory / self.filename
@property
def asset_caches(self):
"""Return an iterator to get all asset caches"""
return (asset_cache for file_cache in self for asset_cache in file_cache)
@property
def tmp_filepath(self):
return Path(bpy.app.tempdir) / self.filename
def read(self):
print(f"Read cache from {self.filepath}")
for file_cache_data in read_file(self.filepath):
self.add(file_cache_data)
return self
def write(self, temp=False):
filepath = self.filepath
if temp:
filepath = self.tmp_filepath
print(f"Write cache file to {filepath}")
write_file(filepath, self._data)
return filepath
def add(self, file_cache_data):
file_cache = FileCache(self, file_cache_data)
self._data.append(file_cache)
def unflatten_cache(self, cache):
"""Return a new unflattten list of asset data
grouped by filepath"""
new_cache = []
cache = deepcopy(cache)
cache.sort(key=lambda x: x["filepath"])
groups = groupby(cache, key=lambda x: x["filepath"])
keys = ["filepath", "modified", "library_id"]
for _, asset_datas in groups:
asset_datas = list(asset_datas)
# print(asset_datas[0])
asset_info = {k: asset_datas[0][k] for k in keys}
asset_info["assets"] = [
{k: v for k, v in a.items() if k not in keys + ["operation"]}
for a in asset_datas
]
new_cache.append(asset_info)
return new_cache
def diff(self, new_cache):
"""Compare the library cache with it current state and return the cache differential"""
cache = self.read()
cache_dict = {f"{a['filepath']}/{a['name']}": a for a in cache.asset_caches}
new_cache_dict = {
f"{a['filepath']}/{a['name']}": a for a in new_cache.asset_caches
}
assets_added = [
AssetCacheDiff(v, "ADD") for k, v in new_cache.items() if k not in cache
]
assets_removed = [
AssetCacheDiff(v, "REMOVED") for k, v in cache.items() if k not in new_cache
]
assets_modified = [
AssetCacheDiff(v, "MODIFIED")
for k, v in cache.items()
if v not in assets_removed and v != new_cache[k]
]
if assets_added:
print(
f"{len(assets_added)} Assets Added \n{tuple(a.name for a in assets_added[:10])}...\n"
)
if assets_removed:
print(
f"{len(assets_removed)} Assets Removed \n{tuple(a.name for a in assets_removed[:10])}...\n"
)
if assets_modified:
print(
f"{len(assets_modified)} Assets Modified \n{tuple(a.name for a in assets_modified[:10])}...\n"
)
cache_diff = LibraryCacheDiff()
cache_diff.set(assets_added + assets_removed + assets_modified)
if not len(LibraryCacheDiff):
print("No change in the library")
return cache_diff
def __len__(self):
return len(self._data)
def __iter__(self):
return iter(self._data)
def __getitem__(self, key):
return self._data[key]
def __str__(self):
return f"LibraryCache(library={self.library.name})"
print()
prefs = bpy.context.preferences.addons["asset_library"].preferences
library = prefs.env_libraries[0]
library_cache = LibraryCache.from_library(library).read()
data = library.library_type.fetch()
print(data)
print(library_cache[0][0])
# library_cache.diff(library.library_type.fetch())
# print(library_cache[0])
+125
View File
@@ -0,0 +1,125 @@
import bpy
from bpy.types import Menu
from .core.lib_utils import get_active_library
class ASSETLIB_MT_node_editor(Menu):
bl_label = "Asset"
def draw(self, context):
layout = self.layout
op = layout.operator("assetlibrary.publish_asset", text='Publish Node Group', icon='NODETREE')
op.data_type = 'NodeTree'
if context.space_data.tree_type == 'GeometryNodeTree':
op = layout.operator("assetlibrary.publish_asset", text='Publish Object', icon='OBJECT_DATA')
op.data_type = 'Object'
elif context.space_data.tree_type == 'ShaderNodeTree':
op = layout.operator("assetlibrary.publish_asset", text='Publish Material', icon='MATERIAL')
op.data_type = 'Material'
def draw_assetbrowser_header(self, context):
lib = get_active_library()
if not lib:
FILEBROWSER_HT_header._draw_asset_browser_buttons(self, context)
return
space_data = context.space_data
params = context.space_data.params
row = self.layout.row(align=True)
row.separator()
row.operator("assetlibrary.bundle", icon='UV_SYNC_SELECT', text='').name = lib.name
#op
#op.clean = False
#op.only_recent = True
lib.plugin.draw_header(row)
if context.selected_files and context.active_file:
row.separator()
row.label(text=context.active_file.name)
row.separator_spacer()
sub = row.row()
sub.ui_units_x = 10
sub.prop(params, "filter_search", text="", icon='VIEWZOOM')
row.separator_spacer()
row.prop_with_popover(
params,
"display_type",
panel="ASSETBROWSER_PT_display",
text="",
icon_only=True,
)
row.operator(
"screen.region_toggle",
text="",
icon='PREFERENCES',
depress=is_option_region_visible(context, space_data)
).region_type = 'TOOL_PROPS'
def draw_assetbrowser_asset_menu(self, context):
layout = self.layout
layout.operator("assetlibrary.publish_assets", text='Publish Assets', icon='ASSET_MANAGER')
# if not get_active_library():
# return
# self.layout.separator()
# box = self.layout.box()
# row = box.row()
# row.separator(factor=0.5)
# row.label(text='Asset Library')
# row.separator(factor=0.5)
# classes = (,
# # ASSETLIB_PT_pose_library_editing,
# # ASSETLIB_PT_pose_library_usage,
# # ASSETLIB_MT_context_menu,
# # ASSETLIB_PT_libraries
# )
def draw_asset_preview_menu(self, context):
layout = self.layout
layout.operator("assetlibrary.save_asset_preview")
def draw_node_tree_menu(self, context):
layout = self.layout
row = layout.row(align=False)
row.menu('ASSETLIB_MT_node_editor')
bl_classes = (
ASSETLIB_MT_node_editor,)
def register() -> None:
for bl_class in bl_classes:
bpy.utils.register_class(bl_class)
#bpy.types.ASSETBROWSER_MT_editor_menus.append(draw_assetbrowser_header)
bpy.types.ASSETBROWSER_MT_metadata_preview_menu.append(draw_asset_preview_menu)
bpy.types.NODE_MT_editor_menus.append(draw_node_tree_menu)
bpy.types.ASSETBROWSER_MT_asset.append(draw_assetbrowser_asset_menu)
def unregister() -> None:
for bl_class in reversed(bl_classes):
bpy.utils.unregister_class(bl_class)
bpy.types.ASSETBROWSER_MT_editor_menus.remove(draw_assetbrowser_header)
bpy.types.NODE_MT_editor_menus.remove(draw_node_tree_menu)
bpy.types.ASSETBROWSER_MT_asset.remove(draw_assetbrowser_asset_menu)