diff --git a/__init__.py b/__init__.py index 693c1b1..287cef4 100644 --- a/__init__.py +++ b/__init__.py @@ -2,7 +2,7 @@ """ -Extending features of the Asset Browser for a studio use. +Extending features of the Asset Browser for a studio use. """ bl_info = { @@ -16,16 +16,17 @@ bl_info = { "category": "Animation", } -#from typing import List, Tuple +# from typing import List, Tuple from asset_library import pose from asset_library import action from asset_library import collection from asset_library import file -from asset_library import (gui, keymaps, preferences, operators) +from asset_library import gui, keymaps, preferences, operators from asset_library import constants -#from asset_library.common.library_type import LibraryType + +# 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 @@ -33,7 +34,7 @@ from asset_library.common.template import Template import re -if 'bpy' in locals(): +if "bpy" in locals(): print("Reload Addon Asset Library") import importlib @@ -54,41 +55,27 @@ import bpy import os -#addon_keymaps: List[Tuple[bpy.types.KeyMap, bpy.types.KeyMapItem]] = [] +# addon_keymaps: List[Tuple[bpy.types.KeyMap, bpy.types.KeyMapItem]] = [] -bl_modules = ( - operators, - pose, - action, - collection, - file, - keymaps, - gui, - preferences -) +bl_modules = (operators, pose, action, collection, file, keymaps, gui, preferences) def load_handler(): - print('load_handler') + print("load_handler") set_env_libraries() bpy.ops.assetlib.set_paths(all=True) if not bpy.app.background: - bpy.ops.assetlib.bundle(blocking=False, mode='AUTO_BUNDLE') - + bpy.ops.assetlib.bundle(blocking=False, mode="AUTO_BUNDLE") def register() -> None: - for m in bl_modules: m.register() - #prefs = get_addon_prefs() - - - + # prefs = get_addon_prefs() bpy.app.timers.register(load_handler, first_interval=1) @@ -99,5 +86,3 @@ def unregister() -> None: for m in reversed(bl_modules): m.unregister() - - diff --git a/action/__init__.py b/action/__init__.py index b55d078..127f851 100644 --- a/action/__init__.py +++ b/action/__init__.py @@ -1,4 +1,3 @@ - from asset_library.action import ( gui, keymaps, @@ -7,10 +6,10 @@ from asset_library.action import ( operators, properties, rename_pose, - #render_preview - ) + # render_preview +) -if 'bpy' in locals(): +if "bpy" in locals(): import importlib importlib.reload(gui) @@ -20,14 +19,16 @@ if 'bpy' in locals(): importlib.reload(operators) importlib.reload(properties) importlib.reload(rename_pose) - #importlib.reload(render_preview) + # importlib.reload(render_preview) import bpy + def register(): operators.register() keymaps.register() + def unregister(): operators.unregister() - keymaps.unregister() \ No newline at end of file + keymaps.unregister() diff --git a/action/_render_preview.py b/action/_render_preview.py index c5436d7..a884c1f 100644 --- a/action/_render_preview.py +++ b/action/_render_preview.py @@ -1,8 +1,7 @@ - - import sys 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.common.file_utils import open_file @@ -19,17 +18,19 @@ import subprocess from tempfile import gettempdir - def rm_tree(pth): pth = Path(pth) - for child in pth.glob('*'): + for child in pth.glob("*"): if child.is_file(): child.unlink() else: rm_tree(child) 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 rnd = bpy.context.scene.render @@ -39,27 +40,32 @@ def render_preview(directory, asset_catalog, render_actions, publish_actions, re blendfile = Path(bpy.data.filepath) asset_catalog_data = read_catalog(asset_catalog) - anim_render_dir = Path(gettempdir()) / 'actionlib_render' #/tmp/actionlib_render. Removed at the end + anim_render_dir = ( + Path(gettempdir()) / "actionlib_render" + ) # /tmp/actionlib_render. Removed at the end 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: rm_tree(preview_render_dir) 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) - - for f in preview_render_dir.rglob('*'): + + for f in preview_render_dir.rglob("*"): if f.is_dir(): - print(f'{f} is dir. Skipped.') + print(f"{f} is dir. Skipped.") continue - if all(i not in f.parts for i in ('anim', 'pose')) and f.parent.parts[-1] != 'preview': - print(f'{f} is out of pipe. Approved or Rtk pictures. Skipped.') + if ( + all(i not in f.parts for i in ("anim", "pose")) + and f.parent.parts[-1] != "preview" + ): + print(f"{f} is out of pipe. Approved or Rtk pictures. Skipped.") continue 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() # Set Scene @@ -68,9 +74,9 @@ def render_preview(directory, asset_catalog, render_actions, publish_actions, re scn.use_preview_range = True scn.eevee.use_gtao = True scn.tool_settings.use_keyframe_insert_auto = False - + # Render Setting - rnd.engine = 'BLENDER_EEVEE' + rnd.engine = "BLENDER_EEVEE" rnd.use_simplify = False rnd.use_stamp_date = True rnd.use_stamp_time = True @@ -89,7 +95,7 @@ def render_preview(directory, asset_catalog, render_actions, publish_actions, re rnd.use_stamp = True rnd.stamp_font_size = 16 rnd.use_stamp_labels = False - rnd.image_settings.file_format = 'JPEG' + rnd.image_settings.file_format = "JPEG" # Viewport Look # ---------- @@ -108,94 +114,104 @@ def render_preview(directory, asset_catalog, render_actions, publish_actions, re """ # Cycles Mat Shading 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].region_3d.view_perspective = 'CAMERA' + a.spaces[0].region_3d.view_perspective = "CAMERA" 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_valley_factor = 1.0 - # Add Subsurf # ----------- - deform_ob = [m.object for o in scn.objects \ - for m in o.modifiers if m.type == 'MESH_DEFORM' + deform_ob = [ + m.object for o in scn.objects for m in o.modifiers if m.type == "MESH_DEFORM" ] - deform_ob += [m.target for o in scn.objects \ - for m in o.modifiers if m.type == 'SURFACE_DEFORM' + deform_ob += [ + m.target for o in scn.objects for m in o.modifiers if m.type == "SURFACE_DEFORM" ] - - objects = [o 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[:]) + + objects = [ + o + 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: subsurf = False for m in o.modifiers: - if m.type == 'SUBSURF': + if m.type == "SUBSURF": m.show_viewport = m.show_render m.levels = m.render_levels subsurf = True break if not subsurf: - subsurf = o.modifiers.new('', 'SUBSURF') + subsurf = o.modifiers.new("", "SUBSURF") subsurf.show_viewport = subsurf.show_render subsurf.levels = subsurf.render_levels - # 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] - rig.animation_data_create() for action_name in render_actions: action = bpy.data.actions.get(action_name) if not action: - print(f'\'{action_name}\' not found.') + print(f"'{action_name}' not found.") continue print(f"-- Current --: {action.name}") - - rnd.stamp_note_text = '{type} : {pose_name}' + + rnd.stamp_note_text = "{type} : {pose_name}" 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.") continue - - 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]) - filename = bpy.path.clean_name(f'{catalog_name}_{action.name}') - ext = 'jpg' + + 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]) + filename = bpy.path.clean_name(f"{catalog_name}_{action.name}") + ext = "jpg" rig.animation_data.action = None bpy.context.view_layer.update() for b in rig.pose.bones: - if re.match('^[A-Z]+\.', b.name): + if re.match("^[A-Z]+\.", b.name): continue reset_bone(b) rest_pose = None - if isinstance(action.asset_data.get('rest_pose'), str): - rest_pose = bpy.data.actions.get(action.asset_data['rest_pose']) - + if isinstance(action.asset_data.get("rest_pose"), str): + rest_pose = bpy.data.actions.get(action.asset_data["rest_pose"]) + rig.animation_data.action = rest_pose bpy.context.view_layer.update() - + rig.animation_data.action = action - if 'camera' in action.asset_data.keys(): - action_cam = bpy.data.objects.get(action.asset_data['camera'], '') + if "camera" in action.asset_data.keys(): + action_cam = bpy.data.objects.get(action.asset_data["camera"], "") if action_cam: scn.camera = action_cam # 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) if not keyframes: continue @@ -203,58 +219,66 @@ def render_preview(directory, asset_catalog, render_actions, publish_actions, re anim_end = keyframes[-1] if anim_start < scn.frame_start: - report.append(f"Issue found for '{action.name}'. Has keyframes before 'Start Frame'.") + report.append( + f"Issue found for '{action.name}'. Has keyframes before 'Start Frame'." + ) continue - + scn.frame_preview_start = anim_start scn.frame_preview_end = anim_end rnd.stamp_note_text = rnd.stamp_note_text.format( - type='ANIM', + type="ANIM", 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) ffmpeg_cmd = [ - 'ffmpeg', '-y', - '-start_number', f'{anim_start:04d}', - '-i', rnd.filepath.replace('####', '%04d'), - '-c:v', 'libx264', - str((preview_render_dir/'anim'/filename).with_suffix('.mov')), + "ffmpeg", + "-y", + "-start_number", + f"{anim_start:04d}", + "-i", + rnd.filepath.replace("####", "%04d"), + "-c:v", + "libx264", + str((preview_render_dir / "anim" / filename).with_suffix(".mov")), ] subprocess.call(ffmpeg_cmd) - + # 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 - + rnd.stamp_note_text = rnd.stamp_note_text.format( - type='POSE', + type="POSE", 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) - filename = rnd.filepath.replace('####', f'{scn.frame_preview_end:04d}') - Path(filename).rename(re.sub('_[0-9]{4}.', '.', filename)) + filename = rnd.filepath.replace("####", f"{scn.frame_preview_end:04d}") + Path(filename).rename(re.sub("_[0-9]{4}.", ".", filename)) shutil.rmtree(anim_render_dir) - + # Report # ------ if report: - report_file = blendfile.parent / Path(f'{blendfile.stem}report').with_suffix('.txt') + report_file = blendfile.parent / Path(f"{blendfile.stem}report").with_suffix( + ".txt" + ) if not report_file.exists(): report_file.touch(exist_ok=False) - report_file.write_text('-') - report_file.write_text('\n'.join(report)) - + report_file.write_text("-") + report_file.write_text("\n".join(report)) + result = report_file else: @@ -262,31 +286,40 @@ def render_preview(directory, asset_catalog, render_actions, publish_actions, re 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( - files=files, catalog_data=asset_catalog_data, - row=2, columns=2, auto_calculate=True, - bg_color=(0.18, 0.18, 0.18,), resize_output=100 + files=files, + catalog_data=asset_catalog_data, + row=2, + columns=2, + auto_calculate=True, + bg_color=( + 0.18, + 0.18, + 0.18, + ), + resize_output=100, ) bpy.ops.wm.quit_blender() +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="Add Comment To the tracker", + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + ) -if __name__ == '__main__' : - parser = argparse.ArgumentParser(description='Add Comment To the tracker', - formatter_class=argparse.ArgumentDefaultsHelpFormatter) + parser.add_argument("--directory") + 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") - parser.add_argument('--directory') - 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:]] + if "--" in sys.argv: + index = sys.argv.index("--") + sys.argv = [sys.argv[index - 1], *sys.argv[index + 1 :]] args = parser.parse_args() render_preview(**vars(args)) diff --git a/action/clear_asset.py b/action/clear_asset.py index 72b4e38..743afed 100644 --- a/action/clear_asset.py +++ b/action/clear_asset.py @@ -1,25 +1,26 @@ - import argparse import bpy import json import sys from pathlib import Path -#sys.path.append(str(Path(__file__).parents[3])) + +# 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): - + +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.') + print(f"No {action_name} not found.") bpy.ops.wm.quit_blender() - action.asset_clear() + action.asset_clear() if use_fake_user: action.use_fake_user = True else: @@ -27,22 +28,22 @@ def clear_asset(action_name='', use_fake_user=False): if preview: preview.unlink() bpy.data.actions.remove(action) - - bpy.ops.wm.save_mainfile( - filepath=bpy.data.filepath, compress=True, exit=True + + 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, ) - - -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') + 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:]] + 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)) diff --git a/action/concat_preview.py b/action/concat_preview.py index fc9c806..18a93a1 100644 --- a/action/concat_preview.py +++ b/action/concat_preview.py @@ -1,4 +1,3 @@ - import bpy import math import numpy as np @@ -10,75 +9,90 @@ def alpha_to_color(pixels_data, color): """Convert Alpha to WhiteBG""" new_pixels_data = [] for i in pixels_data: - height, width, array_d = i.shape - mask = i[:,:,3:] - background = np.array([color[0], color[1], color[2] ,1], dtype=np.float32) - background = np.tile(background, (height*width)) - background = np.reshape(background, (height,width,4)) + height, width, array_d = i.shape + mask = i[:, :, 3:] + background = np.array([color[0], color[1], color[2], 1], dtype=np.float32) + background = np.tile(background, (height * width)) + background = np.reshape(background, (height, width, 4)) new_pixels_data.append(i * mask + background * (1 - mask)) # print(new_pixels_data)#Dbg return new_pixels_data + 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): img_w, img_h = img.size - if img_w != source_width : - scale = abs(img_w/source_width) - img.scale(int(img_w/scale), int(img_h/scale)) + if img_w != source_width: + scale = abs(img_w / source_width) + img.scale(int(img_w / scale), int(img_h / scale)) img_w, img_h = img.size array = create_array(img_h, img_w) img.pixels.foreach_get(array) array = array.reshape(img_h, img_w, 4) - + if array.shape[0] != source_height: - #print('ARRAY SHAPE', array.shape[:], source_height) - missing_height = int(abs(source_height-img_h)/2) + # print('ARRAY SHAPE', array.shape[:], source_height) + missing_height = int(abs(source_height - img_h) / 2) empty_array = create_array(missing_height, source_width) empty_array = empty_array.reshape(missing_height, source_width, 4) array = np.vstack((empty_array, array, empty_array)) return array.reshape(source_height, source_width, 4) + def create_final(output_name, pixels_data, final_height, final_width): - #print('output_name: ', output_name) + # print('output_name: ', output_name) new_img = bpy.data.images.get(output_name) if new_img: bpy.data.images.remove(new_img) new_img = bpy.data.images.new(output_name, final_width, final_height) - new_img.generated_color=(0,0,0,0) + new_img.generated_color = (0, 0, 0, 0) - #print('pixels_data: ', pixels_data) + # print('pixels_data: ', pixels_data) new_img.pixels.foreach_set(pixels_data) return new_img + def guess_input_format(img_list): for i in img_list: if i.size[0] == i.size[1]: return i.size + def format_files(files, catalog_data): img_dict = {} for k, v in catalog_data.items(): - if '/' not in k: + if "/" not in k: 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 + def mosaic_export( - files, catalog_data, row=2, columns=2, auto_calculate=True, - bg_color=(0.18, 0.18, 0.18,), resize_output=100, + files, + catalog_data, + 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) - + for cat, files_list in img_dict.items(): if not files_list: @@ -86,61 +100,64 @@ def mosaic_export( for i in bpy.data.images: bpy.data.images.remove(i) - + img_list = [] - + chars = Path(files_list[0]).parts[-4] output_dir = str(Path(files_list[0]).parent.parent) - ext = 'jpg' - output_name = f'{chars}_{cat}.{ext}' + ext = "jpg" + output_name = f"{chars}_{cat}.{ext}" for img in files_list: img_list.append(bpy.data.images.load(img, check_existing=True)) for i in img_list: - i.colorspace_settings.name = 'Raw' + i.colorspace_settings.name = "Raw" if auto_calculate: 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): + raise AttributeError("Grid too small for number of images") - if rows*columns < len(img_list): - raise AttributeError('Grid too small for number of images') - src_w, src_h = img_list[0].size final_w = src_w * columns final_h = src_h * rows img_pixels = [read_pixels_data(img, src_h, src_w) for img in img_list] - - #Check if there is enough "data" to create an horizontal stack + + # Check if there is enough "data" to create an horizontal stack ##It not, create empty array h_stack = [] - total_len = rows*columns + total_len = rows * columns if len(img_pixels) < total_len: - for i in range(total_len-len(img_pixels)): + for i in range(total_len - len(img_pixels)): img_pixels.append(create_array(src_h, src_w).reshape(src_h, src_w, 4)) - + img_pixels = alpha_to_color(img_pixels, bg_color) - for i in range(0,len(img_pixels),columns): - h_stack.append(np.hstack(img_pixels[i:i+columns])) + for i in range(0, len(img_pixels), columns): + h_stack.append(np.hstack(img_pixels[i : i + columns])) if rows > 1: combined_stack = np.vstack(h_stack[::-1]) else: combined_stack = np.hstack((h_stack[:])) - combined_img = create_final(output_name, combined_stack.flatten(), final_h, final_w) + combined_img = create_final( + output_name, combined_stack.flatten(), final_h, final_w + ) if resize_output != 100: w, h = combined_img.size - combined_img.scale(w*(resize_output*.01), h*(resize_output*.01)) - + combined_img.scale(w * (resize_output * 0.01), h * (resize_output * 0.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() - print(f""" + print( + f""" Image saved: {combined_img.filepath_raw} - """) + """ + ) diff --git a/action/functions.py b/action/functions.py index c8a24e2..c04061e 100644 --- a/action/functions.py +++ b/action/functions.py @@ -13,15 +13,7 @@ import functools import re 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 @@ -30,6 +22,7 @@ FCurveValue = Union[float, int] pose_bone_re = re.compile(r'pose.bones\["([^"]+)"\]') """RegExp for matching FCurve data paths.""" + def is_pose(action): for fc in action.fcurves: if len(fc.keyframe_points) > 1: @@ -37,16 +30,18 @@ def is_pose(action): return True + def get_bone_visibility(data_path): bone, prop = split_path(data_path) - + ob = bpy.context.object b_layers = [i for i, val in enumerate(ob.pose.bones[bone].bone.layers) if val] - + rig_layers = [(i, val) for i, val in enumerate(ob.data.layers)] return ob.data.layers[b_layers[0]] + def get_keyframes(action, selected=False, includes=[]): 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)]) @@ -60,19 +55,26 @@ def get_keyframes(action, selected=False, includes=[]): continue if not get_bone_visibility(f.data_path): continue - - keyframes += [int(k.co[0])] + + keyframes += [int(k.co[0])] if len(keyframes) <= 1: keyframes = [bpy.context.scene.frame_current] else: - keyframes = sorted([int(k.co[0]) for f in action.fcurves for k in f.keyframe_points]) + keyframes = sorted( + [int(k.co[0]) for f in action.fcurves for k in f.keyframe_points] + ) return keyframes + def get_marker(action): if action.pose_markers: markers = action.pose_markers - return next((m.name for m in markers if m.frame == bpy.context.scene.frame_current), None) + return next( + (m.name for m in markers if m.frame == bpy.context.scene.frame_current), + None, + ) + def reset_bone(bone, transform=True, custom_props=True): if transform: @@ -83,41 +85,44 @@ def reset_bone(bone, transform=True, custom_props=True): bone.rotation_axis_angle = (0, 0, 0, 0) else: bone.rotation_euler = (0, 0, 0) - + bone.scale = (1, 1, 1) - + if custom_props: for key, value in bone.items(): try: id_prop = bone.id_properties_ui(key) except TypeError: continue - + if not isinstance(value, (int, float)) or not id_prop: continue - bone[key] = id_prop.as_dict()['default'] + bone[key] = id_prop.as_dict()["default"] + def is_asset_action(action): return action.asset_data and action.asset_data.catalog_id != str(uuid.UUID(int=0)) + def conform_action(action): - tags = ('pose', 'anim') + tags = ("pose", "anim") if any(tag in action.asset_data.tags.keys() for tag in tags): return 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: - action.asset_data['is_single_frame'] = False + action.asset_data["is_single_frame"] = False break - if action.asset_data['is_single_frame']: - action.asset_data.tags.new('pose') + if action.asset_data["is_single_frame"]: + action.asset_data.tags.new("pose") 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 for fc in action.fcurves: bone, prop = split_path(fc.data_path) @@ -133,19 +138,21 @@ def clean_action(action='', frame_start=0, frame_end=0, excludes=[], includes=[] fc.update() # Remove Keyframe out of range - for k in reversed(fc.keyframe_points): - if int(k.co[0]) not in range(frame_start, frame_end+1): + for k in reversed(fc.keyframe_points): + if int(k.co[0]) not in range(frame_start, frame_end + 1): fc.keyframe_points.remove(k) fc.update() -def append_action(action_path='', action_name=''): - print(f'Loading {action_name} from: {action_path}') + +def append_action(action_path="", action_name=""): + print(f"Loading {action_name} from: {action_path}") with bpy.data.libraries.load(str(action_path), link=False) as (data_from, data_to): data_to.actions = [action_name] return data_to.actions[0] + def apply_anim(action_lib, ob, bones=[]): from mathutils import Vector @@ -153,23 +160,32 @@ def apply_anim(action_lib, ob, bones=[]): if not ob.animation_data: ob.animation_data_create() - + action = ob.animation_data.action - + if not action: action = bpy.data.actions.new(ob.name) ob.animation_data.action = action keys = sorted([k.co[0] for f in action_lib.fcurves for k in f.keyframe_points]) if not keys: - print(f'The action {action_lib.name} has no keyframes') + print(f"The action {action_lib.name} has no keyframes") return first_key = keys[0] key_offset = scn.frame_current - first_key - key_attr = ('type', 'interpolation', 'handle_left_type', 'handle_right_type', - 'amplitude', 'back', 'easing', 'period', 'handle_right', 'handle_left' + key_attr = ( + "type", + "interpolation", + "handle_left_type", + "handle_right_type", + "amplitude", + "back", + "easing", + "period", + "handle_right", + "handle_left", ) for fc in action_lib.fcurves: bone_name, prop_name = split_path(fc.data_path) @@ -182,26 +198,25 @@ def apply_anim(action_lib, ob, bones=[]): action_fc = action.fcurves.new( fc.data_path, 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: 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: 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)) setattr(kf, attr, src_val) fc.update() - + # redraw graph areas for window in bpy.context.window_manager.windows: screen = window.screen for area in screen.areas: - if area.type == 'GRAPH_EDITOR': - area.tag_redraw() \ No newline at end of file + if area.type == "GRAPH_EDITOR": + area.tag_redraw() diff --git a/action/gui.py b/action/gui.py index 0802a54..91a2c73 100644 --- a/action/gui.py +++ b/action/gui.py @@ -1,4 +1,3 @@ - import bpy @@ -6,44 +5,59 @@ 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.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_context = "INVOKE_DEFAULT" - #layout.operator("assetlib.rename_asset", text="Rename Action") + # 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.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.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.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.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("asset.library_refresh") - if params.display_type == 'THUMBNAIL': + + 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''' + """Draw the header of the Asset Browser Window""" 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") diff --git a/action/keymaps.py b/action/keymaps.py index e2ed93e..7063f17 100644 --- a/action/keymaps.py +++ b/action/keymaps.py @@ -1,49 +1,63 @@ - - from typing import List, Tuple import bpy addon_keymaps: List[Tuple[bpy.types.KeyMap, bpy.types.KeyMapItem]] = [] + def register(): wm = bpy.context.window_manager addon = wm.keyconfigs.addon if not addon: return - + km = addon.keymaps.new(name="File Browser Main", space_type="FILE_BROWSER") # DblClick to apply pose. - kmi = km.keymap_items.new("actionlib.apply_selected_action", "LEFTMOUSE", "DOUBLE_CLICK") + kmi = km.keymap_items.new( + "actionlib.apply_selected_action", "LEFTMOUSE", "DOUBLE_CLICK" + ) kmi.properties.flipped = False addon_keymaps.append((km, kmi)) - - kmi = km.keymap_items.new("actionlib.apply_selected_action", "LEFTMOUSE", "DOUBLE_CLICK", alt=True) + + kmi = km.keymap_items.new( + "actionlib.apply_selected_action", "LEFTMOUSE", "DOUBLE_CLICK", alt=True + ) kmi.properties.flipped = True addon_keymaps.append((km, kmi)) - - kmi = km.keymap_items.new("poselib.blend_pose_asset_for_keymap", "LEFTMOUSE", "DOUBLE_CLICK", shift=True) + + kmi = km.keymap_items.new( + "poselib.blend_pose_asset_for_keymap", "LEFTMOUSE", "DOUBLE_CLICK", shift=True + ) kmi.properties.flipped = False addon_keymaps.append((km, kmi)) - - kmi = km.keymap_items.new("poselib.blend_pose_asset_for_keymap", "LEFTMOUSE", "DOUBLE_CLICK", alt=True, shift=True) + + kmi = km.keymap_items.new( + "poselib.blend_pose_asset_for_keymap", + "LEFTMOUSE", + "DOUBLE_CLICK", + alt=True, + shift=True, + ) kmi.properties.flipped = True addon_keymaps.append((km, kmi)) - + 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)) 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)) - kmi = km.keymap_items.new("poselib.pose_asset_select_bones", "S", "PRESS", alt=True, ctrl=True) - kmi.properties.selected_side = 'BOTH' + kmi = km.keymap_items.new( + "poselib.pose_asset_select_bones", "S", "PRESS", alt=True, ctrl=True + ) + kmi.properties.selected_side = "BOTH" addon_keymaps.append((km, kmi)) + def unregister(): for km, kmi in addon_keymaps: km.keymap_items.remove(kmi) - addon_keymaps.clear() \ No newline at end of file + addon_keymaps.clear() diff --git a/action/operators.py b/action/operators.py index 0e2ed30..27e4f10 100644 --- a/action/operators.py +++ b/action/operators.py @@ -24,23 +24,20 @@ from functools import partial from pprint import pprint -from asset_library.pose.pose_creation import( +from asset_library.pose.pose_creation import ( 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.action.functions import ( apply_anim, append_action, clean_action, reset_bone, is_asset_action, - conform_action + conform_action, ) from bpy.props import ( @@ -49,7 +46,7 @@ from bpy.props import ( EnumProperty, PointerProperty, StringProperty, - IntProperty + IntProperty, ) from bpy.types import ( @@ -72,15 +69,15 @@ from asset_library.action.functions import ( ) from asset_library.common.functions import ( - #get_actionlib_dir, - #get_asset_source, - #get_catalog_path, - #read_catalog, - #set_actionlib_dir, - #resync_lib, + # get_actionlib_dir, + # get_asset_source, + # get_catalog_path, + # read_catalog, + # set_actionlib_dir, + # resync_lib, get_active_library, get_active_catalog, - asset_warning_callback + asset_warning_callback, ) from asset_library.common.bl_utils import ( @@ -92,10 +89,10 @@ from asset_library.common.bl_utils import ( get_preview, get_view3d_persp, get_viewport, - #load_assets_from, + # load_assets_from, get_asset_space_params, get_bl_cmd, - get_overriden_col + get_overriden_col, ) from asset_library.common.file_utils import ( @@ -137,17 +134,17 @@ class ACTIONLIB_OT_restore_previous_action(Operator): self._timer = wm.event_timer_add(0.001, window=context.window) wm.modal_handler_add(self) - return {'RUNNING_MODAL'} + return {"RUNNING_MODAL"} def modal(self, context, event): - if event.type != 'TIMER': - return {'RUNNING_MODAL'} + if event.type != "TIMER": + return {"RUNNING_MODAL"} wm = context.window_manager wm.event_timer_remove(self._timer) context.object.pose.apply_pose_from_action(self.pose_action) - return {'FINISHED'} + return {"FINISHED"} class ACTIONLIB_OT_assign_action(Operator): @@ -171,9 +168,9 @@ class ACTIONLIB_OT_assign_action(Operator): class ACTIONLIB_OT_replace_pose(Operator): bl_idname = "actionlib.replace_pose" bl_options = {"REGISTER", "UNDO", "INTERNAL"} - bl_label = 'Update Pose' - bl_description = 'Update selected Pose. ! Works only on Pose, not Anim !' - + bl_label = "Update Pose" + bl_description = "Update selected Pose. ! Works only on Pose, not Anim !" + @classmethod def poll(cls, context: Context) -> bool: # wm = context.window_manager @@ -183,23 +180,28 @@ class ACTIONLIB_OT_replace_pose(Operator): # else: # cls.poll_message_set(f"Current Action {context.id.name} different than Edit Action {wm.edit_pose_action}") # return False - if context.mode == 'POSE' and context.area.ui_type == 'ASSETS' and context.active_file: + if ( + context.mode == "POSE" + and context.area.ui_type == "ASSETS" + and context.active_file + ): return True def execute(self, context: Context) -> Set[str]: wm = context.window_manager active = context.active_file - #print('active: ', active) + # print('active: ', active) select_bones( - context.object, - context.asset_file_handle.local_id, - selected_side='BOTH', - toggle=False) + context.object, + context.asset_file_handle.local_id, + selected_side="BOTH", + toggle=False, + ) data = { - 'name':active.name, - 'catalog_id':active.asset_data.catalog_id, + "name": active.name, + "catalog_id": active.asset_data.catalog_id, } data.update(dict(active.asset_data)) @@ -207,12 +209,12 @@ class ACTIONLIB_OT_replace_pose(Operator): # data.update({'camera':active.asset_data['camera']}) # if 'is_single_frame' in asset_data.keys(): # data.update({'is_single_frame' : active.asset_data['is_single_frame']}) - - #print('data: ', data) + + # print('data: ', data) action = create_pose_asset_from_context( context, - data['name'], + data["name"], ) if not action: self.report( # type: ignore @@ -226,57 +228,60 @@ class ACTIONLIB_OT_replace_pose(Operator): _old_action.use_fake_user = False bpy.data.actions.remove(_old_action) - action.name = data['name'] - action.asset_data.catalog_id = data['catalog_id'] + action.name = data["name"] + action.asset_data.catalog_id = data["catalog_id"] 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 - - if not is_pose(action) and 'pose' in action.asset_data.tags.keys() and 'anim' not in action.asset_data.tags.keys(): + + if ( + 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: - if tag != 'pose': + if tag != "pose": continue 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): bl_idname = "actionlib.apply_anim" bl_options = {"REGISTER", "UNDO", "INTERNAL"} - bl_label = 'Apply Anim' - bl_description = 'Apply selected Anim to selected bones' + bl_label = "Apply Anim" + bl_description = "Apply selected Anim to selected bones" @classmethod def poll(cls, context: Context) -> bool: - return context.mode == 'POSE' - + return context.mode == "POSE" + def execute(self, context: Context) -> Set[str]: ob = context.object active_action = context.active_file to_remove = False prefs = get_addon_prefs() - #params = get_asset_space_params(context.area) + # params = get_asset_space_params(context.area) asset_library_ref = context.asset_library_ref - if asset_library_ref == 'LOCAL': + if asset_library_ref == "LOCAL": action = bpy.data.actions[active_action.name] to_remove = False else: asset_file_handle = bpy.context.asset_file_handle if asset_file_handle is None: - return {'CANCELLED'} + return {"CANCELLED"} if asset_file_handle.local_id: - return {'CANCELLED'} + return {"CANCELLED"} lib = get_active_library() - if 'filepath' in asset_file_handle.asset_data: - action_path = asset_file_handle.asset_data['filepath'] + if "filepath" in asset_file_handle.asset_data: + action_path = asset_file_handle.asset_data["filepath"] action_path = lib.library_type.format_path(action_path) else: action_path = bpy.types.AssetHandle.get_full_library_path( @@ -290,7 +295,9 @@ class ACTIONLIB_OT_apply_anim(Operator): ) to_remove = True else: - self.report({"WARNING"}, f"Could not load action path {action_path} not exist") + self.report( + {"WARNING"}, f"Could not load action path {action_path} not exist" + ) return {"CANCELLED"} bones = [b.name for b in context.selected_pose_bones_from_active_object] @@ -298,7 +305,7 @@ class ACTIONLIB_OT_apply_anim(Operator): if to_remove: bpy.data.actions.remove(action) - return {'FINISHED'} + return {"FINISHED"} """ @@ -475,12 +482,14 @@ class ACTIONLIB_OT_create_anim_asset(Operator): # No asset browser is visible, so there also aren't any expectations # that this asset will be visible. return True - + params = get_asset_space_params(asset_browse_area) - if params.asset_library_ref != 'LOCAL': - cls.poll_message_set("Asset Browser must be set to the Current File library") + if params.asset_library_ref != "LOCAL": + cls.poll_message_set( + "Asset Browser must be set to the Current File library" + ) return False - + return True def execute(self, context: Context) -> Set[str]: @@ -490,75 +499,79 @@ class ACTIONLIB_OT_create_anim_asset(Operator): action.asset_generate_preview() data = action.asset_data - #data.catalog_id = str(uuid.UUID(int=0)) + # data.catalog_id = str(uuid.UUID(int=0)) asset_browse_area: Optional[bpy.types.Area] = area_from_context(context) asset_space_params = params(asset_browse_area) - + data.catalog_id = asset_space_params.catalog_id data_dict = dict( 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(): data[k] = v ### - return {'FINISHED'} + return {"FINISHED"} class ACTIONLIB_OT_apply_selected_action(Operator): bl_idname = "actionlib.apply_selected_action" bl_options = {"REGISTER", "UNDO", "INTERNAL"} - bl_label = 'Apply Pose/Anim' - bl_description = 'Apply selected Action to selected bones' + bl_label = "Apply Pose/Anim" + bl_description = "Apply selected Action to selected bones" flipped: BoolProperty(name="Flipped", default=False) # type: ignore @classmethod 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 def execute(self, context: Context) -> Set[str]: 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) else: bpy.ops.actionlib.apply_anim() - - return {'FINISHED'} + + return {"FINISHED"} class ACTIONLIB_OT_edit_action(Operator): bl_idname = "actionlib.edit_action" bl_options = {"REGISTER", "UNDO", "INTERNAL"} - bl_label = 'Edit Action' - bl_description = 'Assign active action and set Camera' + bl_label = "Edit Action" + bl_description = "Assign active action and set Camera" @classmethod def poll(cls, context: Context) -> bool: - if context.mode == 'POSE' and context.area.ui_type == 'ASSETS' and context.active_file: + if ( + context.mode == "POSE" + and context.area.ui_type == "ASSETS" + and context.active_file + ): return True def execute(self, context: Context) -> Set[str]: 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() keyframes = get_keyframes(context.id) 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 scn.frame_set(keyframes[0]) context.object.animation_data.action = None - + for b in context.object.pose.bones: - if re.match('^[A-Z]+\.', b.name): + if re.match("^[A-Z]+\.", b.name): continue reset_bone(b) @@ -566,8 +579,8 @@ class ACTIONLIB_OT_edit_action(Operator): context.view_layer.update() context.object.animation_data.action = context.id - if 'camera' in context.id.asset_data.keys(): - scn.camera = bpy.data.objects[context.id.asset_data['camera']] + if "camera" in context.id.asset_data.keys(): + scn.camera = bpy.data.objects[context.id.asset_data["camera"]] return {"FINISHED"} @@ -575,12 +588,16 @@ class ACTIONLIB_OT_edit_action(Operator): class ACTIONLIB_OT_clear_action(Operator): bl_idname = "actionlib.clear_action" bl_options = {"REGISTER", "UNDO", "INTERNAL"} - bl_label = 'Clear Action' - bl_description = 'Assign active action and set Camera' + bl_label = "Clear Action" + bl_description = "Assign active action and set Camera" @classmethod def poll(cls, context: Context) -> bool: - if context.mode == 'POSE' and context.area.ui_type == 'ASSETS' and context.active_file: + if ( + context.mode == "POSE" + and context.area.ui_type == "ASSETS" + and context.active_file + ): return True def execute(self, context: Context) -> Set[str]: @@ -591,7 +608,7 @@ class ACTIONLIB_OT_clear_action(Operator): context.id.asset_generate_preview() 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() return {"FINISHED"} @@ -600,12 +617,12 @@ class ACTIONLIB_OT_clear_action(Operator): class ACTIONLIB_OT_generate_preview(Operator): bl_idname = "actionlib.generate_preview" bl_options = {"REGISTER", "UNDO", "INTERNAL"} - bl_label = 'Generate Preview' - bl_description = 'Genreate Preview for Active Action' + bl_label = "Generate Preview" + bl_description = "Genreate Preview for Active Action" @classmethod 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 def execute(self, context: Context) -> Set[str]: @@ -616,24 +633,24 @@ class ACTIONLIB_OT_generate_preview(Operator): actions = context.selected_files + [_action] 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: context.object.animation_data.action = rest_pose bpy.context.view_layer.update() else: context.object.animation_data.action = None for b in context.object.pose.bones: - if re.match('^[A-Z]+\.', b.name): + if re.match("^[A-Z]+\.", b.name): continue 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: context.scene.camera = a_cam bpy.context.view_layer.update() a.local_id.asset_generate_preview() - + context.object.animation_data.action = _action context.scene.camera = _camera bpy.context.view_layer.update() @@ -644,15 +661,15 @@ class ACTIONLIB_OT_generate_preview(Operator): class ACTIONLIB_OT_update_action_data(Operator): bl_idname = "actionlib.update_action_data" bl_options = {"REGISTER", "INTERNAL"} - bl_label = 'Udpate Action Data' - bl_description = 'Update Action Metadata' + bl_label = "Udpate Action Data" + bl_description = "Update Action Metadata" tags: EnumProperty( - name='Tags', + name="Tags", items=( - ('POSE', "pose", ""), - ('ANIM', "anim", ""), - ) + ("POSE", "pose", ""), + ("ANIM", "anim", ""), + ), ) use_tags: BoolProperty(default=False) @@ -669,7 +686,7 @@ class ACTIONLIB_OT_update_action_data(Operator): scn.actionlib.camera = scn.camera return context.window_manager.invoke_props_dialog(self) - + def draw(self, context): layout = self.layout layout.use_property_split = True @@ -682,13 +699,13 @@ class ACTIONLIB_OT_update_action_data(Operator): sub = row.row() sub.active = self.use_tags sub.prop(self, "tags", text="") - + heading = layout.column(align=True, heading="Camera") row = heading.row(align=True) row.prop(self, "use_camera", text="") sub = row.row() 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") row = heading.row(align=True) @@ -702,11 +719,11 @@ class ACTIONLIB_OT_update_action_data(Operator): for action in context.selected_files: 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: 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 not_tag in action.asset_data.tags.keys(): @@ -717,46 +734,44 @@ class ACTIONLIB_OT_update_action_data(Operator): action.asset_data.tags.new(tag) - if 'pose' in action.asset_data.tags.keys(): - action.asset_data['is_single_frame'] = True + if "pose" in action.asset_data.tags.keys(): + action.asset_data["is_single_frame"] = True else: - action.asset_data['is_single_frame'] = False - + action.asset_data["is_single_frame"] = False if self.use_rest_pose: - name = scn.actionlib.rest_pose.name if scn.actionlib.rest_pose else '' - action.asset_data['rest_pose'] = name - - return {'FINISHED'} + name = scn.actionlib.rest_pose.name if scn.actionlib.rest_pose else "" + action.asset_data["rest_pose"] = name + return {"FINISHED"} class ACTIONLIB_OT_assign_rest_pose(Operator): bl_idname = "actionlib.mark_as_rest_pose" bl_options = {"REGISTER", "UNDO", "INTERNAL"} - bl_label = 'Mark As Rest Pose' - bl_description = 'Mark Pose as Rest Pose' + bl_label = "Mark As Rest Pose" + bl_description = "Mark Pose as Rest Pose" @classmethod def poll(cls, context: Context) -> bool: - if context.area.ui_type == 'ASSETS': + if context.area.ui_type == "ASSETS": return True def execute(self, context: Context) -> Set[str]: active_action = context.active_file context.scene.actionlib.rest_pose = context.id - + print(f"'{active_action.name.title()}' marked as Rest Pose.") - return {'FINISHED'} + return {"FINISHED"} class ACTIONLIB_OT_open_blendfile(Operator): bl_idname = "actionlib.open_blendfile" bl_options = {"REGISTER", "UNDO", "INTERNAL"} - bl_label = 'Set Paths' - bl_description = 'Open Containing File' - - replace_local : BoolProperty(default=False) + bl_label = "Set Paths" + bl_description = "Open Containing File" + + replace_local: BoolProperty(default=False) @classmethod def poll(cls, context): @@ -764,26 +779,24 @@ class ACTIONLIB_OT_open_blendfile(Operator): return False sp = context.space_data - if sp.params.asset_library_ref == 'LOCAL': + if sp.params.asset_library_ref == "LOCAL": return False return True def execute(self, context: Context) -> Set[str]: asset_path = get_asset_source(replace_local=self.replace_local) - + cmd = get_bl_cmd( blender=bpy.app.binary_path, blendfile=str(asset_path), ) subprocess.Popen(cmd) - return {'FINISHED'} + return {"FINISHED"} - - -#LIBRARY_ITEMS = [] -''' +# LIBRARY_ITEMS = [] +""" def callback_operator(modal_func, operator, override={}): def wrap(self, context, event): @@ -795,7 +808,7 @@ def callback_operator(modal_func, operator, override={}): return retset return wrap -''' +""" class ACTIONLIB_OT_make_custom_preview(Operator): @@ -807,34 +820,47 @@ class ACTIONLIB_OT_make_custom_preview(Operator): prefs = get_addons_prefs() if not prefs.preview_modal: - with context.temp_override(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) + with context.temp_override( + 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 + ) return {"FINISHED"} - return {'PASS_THROUGH'} + return {"PASS_THROUGH"} def invoke(self, context, event): self.source_area = bpy.context.area view3d = get_viewport() - with context.temp_override(area=view3d, region=view3d.regions[-1], window=context.window): + with context.temp_override( + area=view3d, region=view3d.regions[-1], window=context.window + ): # To close the popup bpy.ops.screen.screen_full_area() bpy.ops.screen.back_to_previous() - + view3d = get_viewport() - with context.temp_override(area=view3d, region=view3d.regions[-1], window=context.window): - bpy.ops.assetlib.make_custom_preview('INVOKE_DEFAULT', modal=True) + with context.temp_override( + area=view3d, region=view3d.regions[-1], window=context.window + ): + bpy.ops.assetlib.make_custom_preview("INVOKE_DEFAULT", modal=True) context.window_manager.modal_handler_add(self) - return {'RUNNING_MODAL'} + return {"RUNNING_MODAL"} def get_preview_items(self, context): prefs = get_addon_prefs() - return sorted([(k, k, '', v.icon_id, index) for index, (k, v) in enumerate(prefs.previews.items())], reverse=True) - + return sorted( + [ + (k, k, "", v.icon_id, index) + for index, (k, v) in enumerate(prefs.previews.items()) + ], + reverse=True, + ) class ACTIONLIB_OT_store_anim_pose(Operator): @@ -842,35 +868,43 @@ class ACTIONLIB_OT_store_anim_pose(Operator): bl_label = "Add Action to the current library" bl_description = "Store current pose/anim to local library" - warning: StringProperty(name='') - path: StringProperty(name='Path') - catalog: StringProperty(name='Catalog', update=asset_warning_callback, options={'TEXTEDIT_UPDATE'}) - name: StringProperty(name='Name', update=asset_warning_callback, options={'TEXTEDIT_UPDATE'}) - action_type: EnumProperty(name='Type', items=[('POSE', 'Pose', ''), ('ANIMATION', 'Animation', '')]) + warning: StringProperty(name="") + path: StringProperty(name="Path") + catalog: StringProperty( + name="Catalog", update=asset_warning_callback, options={"TEXTEDIT_UPDATE"} + ) + name: StringProperty( + name="Name", update=asset_warning_callback, options={"TEXTEDIT_UPDATE"} + ) + action_type: EnumProperty( + name="Type", items=[("POSE", "Pose", ""), ("ANIMATION", "Animation", "")] + ) frame_start: IntProperty(name="Frame Start") frame_end: IntProperty(name="Frame End") - tags: StringProperty(name='Tags', description='Tags need to separate with a comma (,)') - description: StringProperty(name='Description') - preview : EnumProperty(items=get_preview_items) - clear_previews : BoolProperty(default=True) - store_library: StringProperty(name='Store Library') - + tags: StringProperty( + name="Tags", description="Tags need to separate with a comma (,)" + ) + description: StringProperty(name="Description") + preview: EnumProperty(items=get_preview_items) + clear_previews: BoolProperty(default=True) + store_library: StringProperty(name="Store Library") + @classmethod def poll(cls, context: Context) -> bool: ob = context.object 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 - if not ob.type == 'ARMATURE': - cls.poll_message_set(f'Active object {ob.name} is not of type ARMATURE') + if not ob.type == "ARMATURE": + cls.poll_message_set(f"Active object {ob.name} is not of type ARMATURE") return False 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 True + return True # def draw_tags(self, asset, layout): # row = layout.row() @@ -882,8 +916,17 @@ class ACTIONLIB_OT_store_anim_pose(Operator): # col.operator("asset.tag_remove", icon='REMOVE', text="") def to_dict(self): - keys = ("catalog", "name", "action_type", "frame_start", "frame_end", "tags", "description", "store_library") - return {k : getattr(self, k) for k in keys} + keys = ( + "catalog", + "name", + "action_type", + "frame_start", + "frame_end", + "tags", + "description", + "store_library", + ) + return {k: getattr(self, k) for k in keys} def draw(self, context): layout = self.layout @@ -891,57 +934,59 @@ class ACTIONLIB_OT_store_anim_pose(Operator): prefs = get_addon_prefs() split = layout.split(factor=0.39, align=True) - #row = split.row(align=False) - #split.use_property_split = False - split.alignment = 'RIGHT' + # row = split.row(align=False) + # split.use_property_split = False + split.alignment = "RIGHT" - split.label(text='Preview') + split.label(text="Preview") sub = split.row(align=True) sub.template_icon_view(self, "preview", show_labels=False) sub.separator() - sub.operator("actionlib.make_custom_preview", icon='RESTRICT_RENDER_OFF', text='') + sub.operator( + "actionlib.make_custom_preview", icon="RESTRICT_RENDER_OFF", text="" + ) prefs.add_asset_dict.clear() prefs.add_asset_dict.update(self.to_dict()) - sub.label(icon='BLANK1') - #layout.ui_units_x = 50 + sub.label(icon="BLANK1") + # layout.ui_units_x = 50 - #row = layout.row(align=True) + # row = layout.row(align=True) layout.use_property_split = True - - if self.current_library.merge_libraries: - layout.prop(self.current_library, 'store_library', expand=False) - #row.label(text='Action Name') + if self.current_library.merge_libraries: + layout.prop(self.current_library, "store_library", expand=False) + + # row.label(text='Action Name') 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.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, "name", text="Name") - #layout.separator() - #self.draw_tags(self.asset_action, layout) - layout.prop(self, 'tags') - layout.prop(self, 'description', text='Description') - #layout.prop(prefs, 'author', text='Author') + # layout.separator() + # self.draw_tags(self.asset_action, layout) + layout.prop(self, "tags") + layout.prop(self, "description", text="Description") + # layout.prop(prefs, 'author', text='Author') - #layout.prop() + # layout.prop() layout.separator() col = layout.column() col.use_property_split = False - #row.enabled = False + # row.enabled = False if self.path: col.label(text=self.path) if self.warning: - col.label(icon='ERROR', text=self.warning) + col.label(icon="ERROR", text=self.warning) def set_action_type(self): ob = bpy.context.object @@ -953,9 +998,9 @@ class ACTIONLIB_OT_store_anim_pose(Operator): self.frame_start = min(keyframes_selected) self.frame_end = max(keyframes_selected) - self.action_type = 'POSE' - if (self.frame_start != self.frame_end): - self.action_type = 'ANIMATION' + self.action_type = "POSE" + if self.frame_start != self.frame_end: + self.action_type = "ANIMATION" def invoke(self, context, event): prefs = get_addon_prefs() @@ -974,15 +1019,14 @@ class ACTIONLIB_OT_store_anim_pose(Operator): self.current_library.store_library = lib.name self.store_library = lib.name - #lib = self.current_library - self.tags = '' + # lib = self.current_library + self.tags = "" - - #print(self, self.library_items) + # print(self, self.library_items) catalog_item = lib.catalog.active_item if catalog_item: - self.catalog = catalog_item.path #get_active_catalog() + self.catalog = catalog_item.path # get_active_catalog() self.set_action_type() @@ -991,8 +1035,8 @@ class ACTIONLIB_OT_store_anim_pose(Operator): view3d = get_viewport() 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: preview_items = get_preview_items(self, context) if preview_items: @@ -1001,28 +1045,28 @@ class ACTIONLIB_OT_store_anim_pose(Operator): return context.window_manager.invoke_props_dialog(self, width=350) def action_to_asset(self, action): - #action.asset_mark() + # action.asset_mark() prefs = get_addon_prefs() action.name = self.name action.asset_generate_preview() - # Remove Keyframes - bones = [b.name for b in bpy.context.selected_pose_bones_from_active_object] + # Remove Keyframes + bones = [b.name for b in bpy.context.selected_pose_bones_from_active_object] clean_action( action=action, frame_start=self.frame_start, frame_end=self.frame_end, - excludes=['world', 'walk'], + excludes=["world", "walk"], includes=bones, ) ## Define Tags - tags = [t.strip() for t in self.tags.split(',') if t] - tag_range = f'f{self.frame_start}' + tags = [t.strip() for t in self.tags.split(",") if t] + tag_range = f"f{self.frame_start}" is_single_frame = True - if self.action_type == 'ANIM': + if self.action_type == "ANIM": 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(tag_range) @@ -1030,16 +1074,16 @@ class ACTIONLIB_OT_store_anim_pose(Operator): for tag in tags: action.asset_data.tags.new(tag) - action.asset_data['is_single_frame'] = is_single_frame - action.asset_data['rig'] = bpy.context.object.name + action.asset_data["is_single_frame"] = is_single_frame + action.asset_data["rig"] = bpy.context.object.name action.asset_data.description = self.description action.asset_data.author = prefs.author col = get_overriden_col(bpy.context.object) if col: - action.asset_data['col'] = col.name - + action.asset_data["col"] = col.name + return action def render_animation(self, video_path): @@ -1050,25 +1094,25 @@ class ACTIONLIB_OT_store_anim_pose(Operator): space = area.spaces.active attrs = [ - (scn, 'use_preview_range', True), - (scn, 'frame_preview_start', self.frame_start), - (scn, 'frame_preview_end', self.frame_end), - (scn.render, 'resolution_percentage', 100), - (space.overlay, 'show_overlays', False), - (space.region_3d, 'view_perspective', 'CAMERA'), - (scn.render, 'resolution_x', 1280), - (scn.render, 'resolution_y', 720), - (scn.render.image_settings, 'file_format', 'FFMPEG'), - (scn.render.ffmpeg, 'format', 'QUICKTIME'), - (scn.render.ffmpeg, 'codec', 'H264'), - (scn.render.ffmpeg, 'ffmpeg_preset', 'GOOD'), - (scn.render.ffmpeg, 'constant_rate_factor', 'HIGH'), - (scn.render.ffmpeg, 'gopsize', 12), - (scn.render, 'filepath', str(video_path)), + (scn, "use_preview_range", True), + (scn, "frame_preview_start", self.frame_start), + (scn, "frame_preview_end", self.frame_end), + (scn.render, "resolution_percentage", 100), + (space.overlay, "show_overlays", False), + (space.region_3d, "view_perspective", "CAMERA"), + (scn.render, "resolution_x", 1280), + (scn.render, "resolution_y", 720), + (scn.render.image_settings, "file_format", "FFMPEG"), + (scn.render.ffmpeg, "format", "QUICKTIME"), + (scn.render.ffmpeg, "codec", "H264"), + (scn.render.ffmpeg, "ffmpeg_preset", "GOOD"), + (scn.render.ffmpeg, "constant_rate_factor", "HIGH"), + (scn.render.ffmpeg, "gopsize", 12), + (scn.render, "filepath", str(video_path)), ] if self.action_type == "ANIMATION": - with attr_set(preview_attrs+video_attrs): + with attr_set(preview_attrs + video_attrs): with ctx.temp_override(area=area): bpy.ops.render.opengl(animation=True) @@ -1076,10 +1120,10 @@ class ACTIONLIB_OT_store_anim_pose(Operator): pass def refresh(self, area): - bpy.ops.asset.library_refresh({"area": area, 'region': area.regions[3]}) - #space_data.activate_asset_by_id(asset, deferred=deferred) + bpy.ops.asset.library_refresh({"area": area, "region": area.regions[3]}) + # space_data.activate_asset_by_id(asset, deferred=deferred) - def execute(self, context: Context): + def execute(self, context: Context): scn = context.scene vl = context.view_layer @@ -1090,12 +1134,16 @@ class ACTIONLIB_OT_store_anim_pose(Operator): lib = self.current_library if lib.merge_libraries: lib = prefs.libraries[self.current_library.store_library] - + lib_type = lib.library_type - + asset_path = lib_type.get_asset_path(name=self.name, catalog=self.catalog) - img_path = lib_type.get_image_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) + img_path = lib_type.get_image_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 current_action = ob.animation_data.action @@ -1105,13 +1153,13 @@ class ACTIONLIB_OT_store_anim_pose(Operator): self.action_to_asset(asset_action) - #lib_type.new_asset() + # lib_type.new_asset() - #Saving the video + # Saving the video if self.action_type == "ANIMATION": self.render_animation(video_path) - - #Saving the preview image + + # Saving the preview image preview = prefs.previews[self.preview] lib_type.write_preview(preview, img_path) @@ -1125,10 +1173,10 @@ class ACTIONLIB_OT_store_anim_pose(Operator): asset_data = dict(lib_type.get_asset_data(asset_action), catalog=self.catalog) asset_info = lib_type.format_asset_info([asset_data], asset_path=asset_path) - #print('asset_info') - #pprint(asset_info) + # print('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 @@ -1137,24 +1185,24 @@ class ACTIONLIB_OT_store_anim_pose(Operator): bpy.data.actions.remove(asset_action) # 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)) bpy.ops.assetlib.bundle(name=lib.name, diff=str(diff_path), blocking=True) - #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"} - + classes = ( ACTIONLIB_OT_assign_action, ACTIONLIB_OT_restore_previous_action, - #ACTIONLIB_OT_publish, + # ACTIONLIB_OT_publish, ACTIONLIB_OT_apply_anim, ACTIONLIB_OT_replace_pose, ACTIONLIB_OT_create_anim_asset, @@ -1165,7 +1213,7 @@ classes = ( ACTIONLIB_OT_update_action_data, ACTIONLIB_OT_assign_rest_pose, ACTIONLIB_OT_store_anim_pose, - ACTIONLIB_OT_make_custom_preview + ACTIONLIB_OT_make_custom_preview, ) register, unregister = bpy.utils.register_classes_factory(classes) diff --git a/action/properties.py b/action/properties.py index 4206e6d..1c8a976 100644 --- a/action/properties.py +++ b/action/properties.py @@ -2,20 +2,19 @@ import bpy from bpy.types import PropertyGroup from bpy.props import PointerProperty, StringProperty, BoolProperty + class ACTIONLIB_PG_scene(PropertyGroup): - flipped : BoolProperty( + flipped: BoolProperty( name="Flip Pose", default=False, ) - previous_action : PointerProperty(type=bpy.types.Action) - publish_path : StringProperty(subtype='FILE_PATH') - 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) + previous_action: PointerProperty(type=bpy.types.Action) + publish_path: StringProperty(subtype="FILE_PATH") + 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) -classes = ( - ACTIONLIB_PG_scene, -) +classes = (ACTIONLIB_PG_scene,) def register(): @@ -24,6 +23,7 @@ def register(): bpy.types.Scene.actionlib = PointerProperty(type=ACTIONLIB_PG_scene) + def unregister(): try: del bpy.types.Scene.actionlib @@ -31,4 +31,4 @@ def unregister(): pass for cls in reversed(classes): - bpy.utils.unregister_class(cls) \ No newline at end of file + bpy.utils.unregister_class(cls) diff --git a/action/rename_pose.py b/action/rename_pose.py index 3c9f722..9329c34 100644 --- a/action/rename_pose.py +++ b/action/rename_pose.py @@ -1,4 +1,3 @@ - import argparse import bpy import json @@ -6,17 +5,19 @@ import re import sys from pathlib import Path -#sys.path.append(str(Path(__file__).parents[3])) + +# sys.path.append(str(Path(__file__).parents[3])) from asset_library.common.bl_utils import ( get_preview, ) -def rename_pose(src_name='', dst_name=''): - + +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.') + print(f"No {src_name} not found.") bpy.ops.wm.quit_blender() action.name = dst_name @@ -24,21 +25,21 @@ def rename_pose(src_name='', dst_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 + 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, ) - -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") - 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:]] + 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)) diff --git a/adapters/adapter.py b/adapters/adapter.py index 6c627d9..cf37ff1 100644 --- a/adapters/adapter.py +++ b/adapters/adapter.py @@ -1,12 +1,15 @@ - - from bpy.types import PropertyGroup class Adapter(PropertyGroup): - #def __init__(self): + # def __init__(self): name = "Base Adapter" - #library = None + + # library = None def to_dict(self): - return {p: getattr(self, p) for p in self.bl_rna.properties.keys() if p !='rna_type'} \ No newline at end of file + return { + p: getattr(self, p) + for p in self.bl_rna.properties.keys() + if p != "rna_type" + } diff --git a/collection/__init__.py b/collection/__init__.py index 9154052..0728768 100644 --- a/collection/__init__.py +++ b/collection/__init__.py @@ -1,27 +1,28 @@ - from asset_library.collection import ( gui, operators, keymaps, - #build_collection_blends, - #create_collection_library, - ) + # build_collection_blends, + # create_collection_library, +) -if 'bpy' in locals(): +if "bpy" in locals(): import importlib importlib.reload(gui) importlib.reload(operators) importlib.reload(keymaps) - #importlib.reload(build_collection_blends) - #importlib.reload(create_collection_library) + # importlib.reload(build_collection_blends) + # importlib.reload(create_collection_library) import bpy + def register(): operators.register() keymaps.register() + def unregister(): operators.unregister() - keymaps.unregister() \ No newline at end of file + keymaps.unregister() diff --git a/collection/build_collection_blends.py b/collection/build_collection_blends.py index 17e0553..bd15de6 100644 --- a/collection/build_collection_blends.py +++ b/collection/build_collection_blends.py @@ -29,12 +29,13 @@ from asset_library.constants import ASSETLIB_FILENAME ] """ + def build_collection_blends(path, categories=None, clean=True): t0 = time() scn = bpy.context.scene scn.render.resolution_x = scn.render.resolution_y = 1000 - + json_path = Path(path) / ASSETLIB_FILENAME if not json_path.exists(): return @@ -43,30 +44,33 @@ def build_collection_blends(path, categories=None, clean=True): category_datas = json.loads(json_path.read_text()) 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 bpy.ops.wm.read_homefile(use_empty=True) + # category_data = next(c for c in category_datas if c['name'] == category) + # _col_datas = category_data['children'] - #category_data = next(c for c in category_datas if c['name'] == category) - #_col_datas = category_data['children'] - - cat_name = category_data['name'] - build_path = Path(path) / cat_name / f'{cat_name}.blend' + cat_name = category_data["name"] + build_path = Path(path) / cat_name / f"{cat_name}.blend" ## re-iterate in grouped filepath - col_datas = sorted(category_data['children'], key=lambda x: x['filepath']) - for filepath, col_data_groups in groupby(col_datas, key=lambda x: x['filepath']): - #f = Path(f) + col_datas = sorted(category_data["children"], key=lambda x: x["filepath"]) + for filepath, col_data_groups in groupby( + col_datas, key=lambda x: x["filepath"] + ): + # f = Path(f) if not Path(filepath).exists(): - print(f'Not exists: {filepath}') + print(f"Not exists: {filepath}") continue col_data_groups = list(col_data_groups) - col_names = [a['name'] for a in col_data_groups] - linked_cols = load_datablocks(filepath, col_names, link=True, type='collections') + col_names = [a["name"] for a in col_data_groups] + linked_cols = load_datablocks( + filepath, col_names, link=True, type="collections" + ) for i, col in enumerate(linked_cols): # iterate in linked collection and associated data @@ -78,14 +82,14 @@ def build_collection_blends(path, categories=None, clean=True): ## Directly link as collection inside a marked collection with same name marked_col = col_as_asset(col, verbose=True) - marked_col.asset_data.description = asset_data.get('description', '') - marked_col.asset_data.catalog_id = category_data['id'] # assign catalog + marked_col.asset_data.description = asset_data.get("description", "") + 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 ## exclude collections and generate preview - bpy.ops.ed.lib_id_generate_preview({"id": marked_col}) # preview gen + bpy.ops.ed.lib_id_generate_preview({"id": marked_col}) # preview gen vcol = bpy.context.view_layer.layer_collection.children[marked_col.name] vcol.exclude = True @@ -93,32 +97,36 @@ def build_collection_blends(path, categories=None, clean=True): ## clear all objects (can be very long with a lot of objects...): if clean: - print('Removing links...') + print("Removing links...") for lib in reversed(bpy.data.libraries): bpy.data.libraries.remove(lib) - - # Créer les dossiers intermediaires 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) - - print("build time:", f'{time() - t0:.1f}s') + + print("build time:", f"{time() - t0:.1f}s") bpy.ops.wm.quit_blender() -if __name__ == '__main__' : - parser = argparse.ArgumentParser(description='build_collection_blends', - formatter_class=argparse.ArgumentDefaultsHelpFormatter) +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="build_collection_blends", + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + ) - parser.add_argument('-path') # 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 + parser.add_argument( + "-path" + ) # 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 : - index = sys.argv.index('--') - sys.argv = [sys.argv[index-1], *sys.argv[index+1:]] + if "--" in sys.argv: + index = sys.argv.index("--") + sys.argv = [sys.argv[index - 1], *sys.argv[index + 1 :]] args = parser.parse_args() - build_collection_blends(**vars(args)) \ No newline at end of file + build_collection_blends(**vars(args)) diff --git a/collection/create_collection_library.py b/collection/create_collection_library.py index 0c218fe..e606d36 100644 --- a/collection/create_collection_library.py +++ b/collection/create_collection_library.py @@ -44,87 +44,93 @@ from asset_library.constants import ASSETLIB_FILENAME ] """ + 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) - ''' + """ json_path = Path(path) / ASSETLIB_FILENAME - + # scan all last version of the assets ? # get last version files ? # 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???) - blends = get_last_files(source_directory, pattern=r'(_v\d{3})?\.blend$', only_matching=True) + blends = get_last_files( + source_directory, pattern=r"(_v\d{3})?\.blend$", only_matching=True + ) - root_path = Path(source_directory).as_posix().rstrip('/') + '/' - print('root_path: ', root_path) + root_path = Path(source_directory).as_posix().rstrip("/") + "/" + print("root_path: ", root_path) # open and check data block marked as asset category_datas = [] for i, blend in enumerate(blends): 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] ? ## Remove root path and extension - ## top level folder ('chars'), problem if blends at root - category = fp.as_posix().replace(root_path, '').split('/')[0] - + ## top level folder ('chars'), problem if blends at root + category = fp.as_posix().replace(root_path, "").split("/")[0] + ## full blend path (chars/perso/perso) # category = fp.as_posix().replace(root_path, '').rsplit('.', 1)[0] - + print(category) - with bpy.data.libraries.load(blend, link=True, assets_only=True) as (data_from, data_to): + with bpy.data.libraries.load(blend, link=True, assets_only=True) as ( + data_from, + data_to, + ): ## just listing col_name_list = [c for c in data_from.collections] if not col_name_list: continue - col_list = next((c['children'] for c in category_datas if c['name'] == category), None) + col_list = next( + (c["children"] for c in category_datas if c["name"] == category), None + ) if col_list is None: col_list = [] category_data = { - 'name': category, - 'id': str(uuid.uuid4()), - 'children': col_list, - } + "name": category, + "id": str(uuid.uuid4()), + "children": col_list, + } category_datas.append(category_data) - blend_source_path = blend.as_posix() - if (project_root := os.environ.get('PROJECT_ROOT')): - blend_source_path = blend_source_path.replace(project_root, '$PROJECT_ROOT') - + if project_root := os.environ.get("PROJECT_ROOT"): + blend_source_path = blend_source_path.replace(project_root, "$PROJECT_ROOT") for name in col_name_list: data = { - 'filepath' : blend, - 'name' : name, + "filepath": blend, + "name": name, # 'tags' : [], - 'metadata' : {'filepath': blend_source_path}, + "metadata": {"filepath": blend_source_path}, } 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_catalog_file(json_path, keep_existing_category=True) def create_collection_library(path, source_directory=None): - ''' + """ path: store collection library (json and blends database) source_directory: if a source is set, rebuild json and library - ''' + """ if source_directory: if not Path(source_directory).exists(): - print(f'Source directory not exists: {source_directory}') + print(f"Source directory not exists: {source_directory}") return ## scan source and build json in assetlib dir root @@ -132,32 +138,45 @@ def create_collection_library(path, source_directory=None): json_path = Path(path) / ASSETLIB_FILENAME if not json_path.exists(): - print(f'No json found at: {json_path}') + print(f"No json found at: {json_path}") return file_datas = json.loads(json_path.read()) ## For each category in json, execute build_assets_blend script - script = Path(__file__).parent / 'build_collection_blends.py' - #empty_blend = Path(__file__).parent / 'empty_scene.blend' - + script = Path(__file__).parent / "build_collection_blends.py" + # empty_blend = Path(__file__).parent / 'empty_scene.blend' + # for category, asset_datas in file_datas.items(): for category_data in file_datas: ## add an empty blend as second arg - cmd = [bpy.app.binary_path, '--python', str(script), '--', '--path', path, '--category', category_data['name']] - print('cmd: ', cmd) + cmd = [ + bpy.app.binary_path, + "--python", + str(script), + "--", + "--path", + path, + "--category", + category_data["name"], + ] + print("cmd: ", cmd) subprocess.call(cmd) -if __name__ == '__main__' : - parser = argparse.ArgumentParser(description='Create Collection Library', - formatter_class=argparse.ArgumentDefaultsHelpFormatter) +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="Create Collection Library", + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + ) - parser.add_argument('--path') # trouve/créer le json assetlib.json en sous-dossier de libdir + parser.add_argument( + "--path" + ) # trouve/créer le json assetlib.json en sous-dossier de libdir - if '--' in sys.argv : - index = sys.argv.index('--') - sys.argv = [sys.argv[index-1], *sys.argv[index+1:]] + if "--" in sys.argv: + index = sys.argv.index("--") + sys.argv = [sys.argv[index - 1], *sys.argv[index + 1 :]] args = parser.parse_args() - create_collection_library(**vars(args)) \ No newline at end of file + create_collection_library(**vars(args)) diff --git a/collection/gui.py b/collection/gui.py index 9bf4669..42761e8 100644 --- a/collection/gui.py +++ b/collection/gui.py @@ -1,4 +1,3 @@ - import bpy @@ -9,6 +8,6 @@ def draw_context_menu(layout): def draw_header(layout): - '''Draw the header of the Asset Browser Window''' + """Draw the header of the Asset Browser Window""" - return \ No newline at end of file + return diff --git a/collection/keymaps.py b/collection/keymaps.py index 04e97c7..6db12ac 100644 --- a/collection/keymaps.py +++ b/collection/keymaps.py @@ -1,11 +1,10 @@ - - from typing import List, Tuple import bpy addon_keymaps: List[Tuple[bpy.types.KeyMap, bpy.types.KeyMapItem]] = [] + def register(): wm = bpy.context.window_manager addon = wm.keyconfigs.addon @@ -13,10 +12,13 @@ def register(): return km = addon.keymaps.new(name="File Browser Main", space_type="FILE_BROWSER") - kmi = km.keymap_items.new("assetlib.load_asset", "LEFTMOUSE", "DOUBLE_CLICK") # , shift=True + kmi = km.keymap_items.new( + "assetlib.load_asset", "LEFTMOUSE", "DOUBLE_CLICK" + ) # , shift=True addon_keymaps.append((km, kmi)) + def unregister(): for km, kmi in addon_keymaps: km.keymap_items.remove(kmi) - addon_keymaps.clear() \ No newline at end of file + addon_keymaps.clear() diff --git a/collection/operators.py b/collection/operators.py index 0f5de03..6945f45 100644 --- a/collection/operators.py +++ b/collection/operators.py @@ -14,24 +14,23 @@ from asset_library.common.bl_utils import load_col from asset_library.common.functions import get_active_library - class ASSETLIB_OT_load_asset(Operator): bl_idname = "assetlib.load_asset" bl_options = {"REGISTER", "UNDO", "INTERNAL"} - bl_label = 'Load Asset' - bl_description = 'Link and override asset in current file' + bl_label = "Load Asset" + bl_description = "Link and override asset in current file" @classmethod def poll(cls, context: Context) -> bool: if not asset_utils.SpaceAssetInfo.is_asset_browser(context.space_data): cls.poll_message_set("Current editor is not an asset browser") return False - + lib = get_active_library() - if not lib or lib.data_type != 'COLLECTION': + if not lib or lib.data_type != "COLLECTION": 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") return False @@ -39,51 +38,49 @@ class ASSETLIB_OT_load_asset(Operator): def execute(self, context: Context) -> Set[str]: - print('Load Asset') + print("Load Asset") lib = get_active_library() - - asset = context.active_file if not asset: - self.report({"ERROR"}, 'No asset selected') - return {'CANCELLED'} - + self.report({"ERROR"}, "No asset selected") + return {"CANCELLED"} + 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) name = asset.name ## set mode to object - if context.mode != 'OBJECT': - bpy.ops.object.mode_set(mode='OBJECT') - + if context.mode != "OBJECT": + bpy.ops.object.mode_set(mode="OBJECT") + if not Path(asset_path).exists(): - self.report({'ERROR'}, f'Not exists: {asset_path}') - return {'CANCELLED'} + self.report({"ERROR"}, f"Not exists: {asset_path}") + return {"CANCELLED"} - print('Load collection', asset_path, name) - res = load_col(asset_path, name, link=True, override=True, rig_pattern='*_rig') + print("Load collection", asset_path, name) + res = load_col(asset_path, name, link=True, override=True, rig_pattern="*_rig") if res: - if res.type == 'ARMATURE': - self.report({'INFO'}, f'Override rig {res.name}') - elif res.type == 'EMPTY': - self.report({'INFO'}, f'Instance collection {res.name}') + if res.type == "ARMATURE": + self.report({"INFO"}, f"Override rig {res.name}") + elif res.type == "EMPTY": + self.report({"INFO"}, f"Instance collection {res.name}") - return {'FINISHED'} + return {"FINISHED"} ### --- REGISTER --- -classes = ( - ASSETLIB_OT_load_asset, -) +classes = (ASSETLIB_OT_load_asset,) + def register(): for cls in classes: bpy.utils.register_class(cls) + def unregister(): for cls in reversed(classes): - bpy.utils.unregister_class(cls) \ No newline at end of file + bpy.utils.unregister_class(cls) diff --git a/common/__init__.py b/common/__init__.py index 0a02c21..302664b 100644 --- a/common/__init__.py +++ b/common/__init__.py @@ -1,12 +1,10 @@ - - 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(): +if "bpy" in locals(): import importlib importlib.reload(file_utils) @@ -15,4 +13,4 @@ if 'bpy' in locals(): importlib.reload(template) importlib.reload(catalog) -import bpy \ No newline at end of file +import bpy diff --git a/common/bl_utils.py b/common/bl_utils.py index 07c1082..80e948c 100644 --- a/common/bl_utils.py +++ b/common/bl_utils.py @@ -1,4 +1,3 @@ - """ Generic Blender functions """ @@ -6,29 +5,31 @@ Generic Blender functions from pathlib import Path from fnmatch import fnmatch from typing import Any, List, Iterable, Optional, Tuple + Datablock = Any import bpy from bpy_extras import asset_utils from asset_library.constants import RESOURCES_DIR -#from asset_library.common.file_utils import no + +# from asset_library.common.file_utils import no from os.path import abspath import subprocess -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) exiting with-statement: Restore values to their old values - ''' + """ def __init__(self, attrib_list): self.store = [] # item = (prop, attr, [new_val]) for item in attrib_list: prop, attr = item[:2] - self.store.append( (prop, attr, getattr(prop, attr)) ) - + self.store.append((prop, attr, getattr(prop, attr))) + for item in attrib_list: prop, attr = item[:2] @@ -36,7 +37,7 @@ class attr_set(): try: setattr(prop, attr, item[2]) except TypeError: - print(f'Cannot set attribute {attr} to {prop}') + print(f"Cannot set attribute {attr} to {prop}") def __enter__(self): return self @@ -48,25 +49,38 @@ class attr_set(): for prop, attr, old_val in self.store: setattr(prop, attr, old_val) + def get_overriden_col(ob, scene=None): scn = scene or bpy.context.scene cols = [c for c in bpy.data.collections if scn.user_of_id(c)] - - return next((c for c in cols if ob in c.all_objects[:] - if all(not c.override_library for c in get_col_parents(c))), None) + + return next( + ( + c + for c in cols + if ob in c.all_objects[:] + if all(not c.override_library for c in get_col_parents(c)) + ), + None, + ) + def get_view3d_persp(): windows = bpy.context.window_manager.windows - view_3ds = [a for w in windows for a in w.screen.areas if a.type == 'VIEW_3D'] - view_3d = next((a for a in view_3ds if a.spaces.active.region_3d.view_perspective == 'PERSP'), view_3ds[0]) + view_3ds = [a for w in windows for a in w.screen.areas if a.type == "VIEW_3D"] + view_3d = next( + (a for a in view_3ds if a.spaces.active.region_3d.view_perspective == "PERSP"), + view_3ds[0], + ) return view_3d + def get_viewport(): screen = bpy.context.screen - areas = [a for a in screen.areas if a.type == 'VIEW_3D'] - areas.sort(key=lambda x : x.width*x.height) + areas = [a for a in screen.areas if a.type == "VIEW_3D"] + areas.sort(key=lambda x: x.width * x.height) return areas[-1] @@ -81,7 +95,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]: """Return area size in pixels.""" - return (area.width * area.height) + return area.width * area.height areas = list(suitable_areas(screen)) if not areas: @@ -89,6 +103,7 @@ def biggest_asset_browser_area(screen: bpy.types.Screen) -> Optional[bpy.types.A return max(areas, key=area_sorting_key) + def suitable_areas(screen: bpy.types.Screen) -> Iterable[bpy.types.Area]: """Generator, yield Asset Browser areas.""" @@ -98,6 +113,7 @@ def suitable_areas(screen: bpy.types.Screen) -> Iterable[bpy.types.Area]: continue yield area + def area_from_context(context: bpy.types.Context) -> Optional[bpy.types.Area]: """Return an Asset Browser suitable for the given category. @@ -122,6 +138,7 @@ def area_from_context(context: bpy.types.Context) -> Optional[bpy.types.Area]: return None + def activate_asset( asset: bpy.types.Action, asset_browser: bpy.types.Area, *, deferred: bool ) -> None: @@ -131,19 +148,25 @@ def activate_asset( assert asset_utils.SpaceAssetInfo.is_asset_browser(space_data) space_data.activate_asset_by_id(asset, deferred=deferred) + def active_catalog_id(asset_browser: bpy.types.Area) -> str: """Return the ID of the catalog shown in the asset browser.""" return params(asset_browser).catalog_id -def get_asset_space_params(asset_browser: bpy.types.Area) -> bpy.types.FileAssetSelectParams: + +def get_asset_space_params( + asset_browser: bpy.types.Area, +) -> bpy.types.FileAssetSelectParams: """Return the asset browser parameters given its Area.""" space_data = asset_browser.spaces[0] assert asset_utils.SpaceAssetInfo.is_asset_browser(space_data) return space_data.params + def refresh_asset_browsers(): for area in suitable_areas(bpy.context.screen): - bpy.ops.asset.library_refresh({"area": area, 'region': area.regions[3]}) + bpy.ops.asset.library_refresh({"area": area, "region": area.regions[3]}) + def tag_redraw(screen: bpy.types.Screen) -> None: """Tag all asset browsers for redrawing.""" @@ -151,6 +174,7 @@ def tag_redraw(screen: bpy.types.Screen) -> None: for area in suitable_areas(screen): area.tag_redraw() + # def get_blender_command(file=None, script=None, background=True, **args): # '''Return a Blender Command as a list to be used in a subprocess''' @@ -166,9 +190,10 @@ def tag_redraw(screen: bpy.types.Screen) -> None: # cmd += ['--'] # for k, v in args.items(): # cmd += [f"--{k.replace('_', '-')}", str(v)] - + # return cmd + def norm_value(value): if isinstance(value, (tuple, list)): values = [] @@ -177,7 +202,7 @@ def norm_value(value): v = json.dumps(v) values.append(v) - return values + return values if isinstance(value, Path): return str(value) @@ -186,31 +211,35 @@ def norm_value(value): value = json.dumps(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) return prefix + arg_name -def get_bl_cmd(blender=None, background=False, focus=True, blendfile=None, script=None, **kargs): + +def get_bl_cmd( + blender=None, background=False, focus=True, blendfile=None, script=None, **kargs +): cmd = [str(blender)] if blender else [bpy.app.binary_path] if background: - cmd += ['--background'] + cmd += ["--background"] if not focus and not background: - cmd += ['--no-window-focus'] - cmd += ['--window-geometry', '5000', '0', '10', '10'] + cmd += ["--no-window-focus"] + cmd += ["--window-geometry", "5000", "0", "10", "10"] - cmd += ['--python-use-system-env'] + cmd += ["--python-use-system-env"] if blendfile: cmd += [str(blendfile)] - + if script: - cmd += ['--python', str(script)] - + cmd += ["--python", str(script)] + if kargs: - cmd += ['--'] + cmd += ["--"] for k, v in kargs.items(): k = norm_arg(k) v = norm_value(v) @@ -223,18 +252,18 @@ def get_bl_cmd(blender=None, background=False, focus=True, blendfile=None, scrip return cmd -def get_addon_prefs(): - addon_name = __package__.split('.')[0] - return bpy.context.preferences.addons[addon_name].preferences +def get_addon_prefs(): + addon_name = __package__.split(".")[0] + return bpy.context.preferences.addons[addon_name].preferences def thumbnail_blend_file(input_blend, output_img): input_blend = Path(input_blend).resolve() output_img = Path(output_img).resolve() - print(f'Thumbnailing {input_blend} to {output_img}') - blender_thumbnailer = Path(bpy.app.binary_path).parent / 'blender-thumbnailer' + 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) @@ -243,16 +272,17 @@ def thumbnail_blend_file(input_blend, output_img): success = output_img.exists() if not success: - empty_preview = RESOURCES_DIR / 'empty_preview.png' + empty_preview = RESOURCES_DIR / "empty_preview.png" shutil.copy(str(empty_preview), str(output_img)) return success + def get_col_parents(col, root=None, cols=None): - '''Return a list of parents collections of passed col + """Return a list of parents collections of passed col root : Pass a collection to search in (recursive) else search in master collection - ''' + """ if cols is None: cols = [] @@ -267,14 +297,23 @@ def get_col_parents(col, root=None, cols=None): cols = get_col_parents(col, root=sub, cols=cols) return cols + def get_overriden_col(ob, scene=None): - '''Get the collection use for making the override''' + """Get the collection use for making the override""" scn = scene or bpy.context.scene cols = [c for c in bpy.data.collections if scn.user_of_id(c)] - - return next((c for c in cols if ob in c.all_objects[:] - if all(not c.override_library for c in get_col_parents(c))), None) + + return next( + ( + c + for c in cols + if ob in c.all_objects[:] + if all(not c.override_library for c in get_col_parents(c)) + ), + None, + ) + def load_assets_from(filepath: Path) -> List[Datablock]: if not has_assets(filepath): @@ -306,6 +345,7 @@ def load_assets_from(filepath: Path) -> List[Datablock]: loaded_assets.append(datablock) return loaded_assets + def has_assets(filepath: Path) -> bool: with bpy.data.libraries.load(str(filepath), assets_only=True) as ( data_from, @@ -318,51 +358,49 @@ def has_assets(filepath: Path) -> bool: return False - - - - - def copy_frames(start, end, offset, path): - for i in range (start, end): - src = path.replace('####', f'{i:04d}') - dst = src.replace(src.split('_')[-1].split('.')[0], f'{i+offset:04d}') + for i in range(start, end): + src = path.replace("####", f"{i:04d}") + dst = src.replace(src.split("_")[-1].split(".")[0], f"{i+offset:04d}") shutil.copy2(src, dst) -def split_path(path) : - try : + +def split_path(path): + try: bone_name = path.split('["')[1].split('"]')[0] - except : + except: bone_name = None - try : + try: prop_name = path.split('["')[2].split('"]')[0] - except : - prop_name = path.split('.')[-1] + except: + prop_name = path.split(".")[-1] return bone_name, prop_name - - - -def load_datablocks(src, names=None, type='objects', link=True, expr=None, assets_only=False) -> list: +def load_datablocks( + src, names=None, type="objects", link=True, expr=None, assets_only=False +) -> list: return_list = not isinstance(names, str) names = names or [] if not isinstance(names, (list, tuple)): names = [names] - + if isinstance(expr, str): pattern = expr - expr = lambda x : fnmatch(x, pattern) - - with bpy.data.libraries.load(str(src), link=link,assets_only=assets_only) as (data_from, data_to): + expr = lambda x: fnmatch(x, pattern) + + with bpy.data.libraries.load(str(src), link=link, assets_only=assets_only) as ( + data_from, + data_to, + ): datablocks = getattr(data_from, type) if expr: names += [i for i in datablocks if expr(i)] elif not names: names = datablocks - + setattr(data_to, type, names) datablocks = getattr(data_to, type) @@ -373,23 +411,26 @@ def load_datablocks(src, names=None, type='objects', link=True, expr=None, asset elif datablocks: return datablocks[0] + """ # --- Collection handling """ + def col_as_asset(col, verbose=False): if col is None: return if verbose: - print('linking:', col.name) + print("linking:", col.name) pcol = bpy.data.collections.new(col.name) bpy.context.scene.collection.children.link(pcol) pcol.children.link(col) pcol.asset_mark() return pcol + def load_col(filepath, name, link=True, override=True, rig_pattern=None, context=None): - '''Link a collection by name from a file and override if has armature''' + """Link a collection by name from a file and override if has armature""" # with bpy.data.libraries.load(filepath, link=link) as (data_from, data_to): # data_to.collections = [c for c in data_from.collections if c == name] @@ -398,12 +439,12 @@ def load_col(filepath, name, link=True, override=True, rig_pattern=None, context # return data_to.collections[0] 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 inst = bpy.data.objects.new(col.name, None) inst.instance_collection = col - inst.instance_type = 'COLLECTION' + inst.instance_type = "COLLECTION" context.scene.collection.objects.link(inst) # make active @@ -413,26 +454,29 @@ def load_col(filepath, name, link=True, override=True, rig_pattern=None, context ## simple object (no armatures) if not link or not override: return inst - if not next((o for o in col.all_objects if o.type == 'ARMATURE'), None): + if not next((o for o in col.all_objects if o.type == "ARMATURE"), None): return inst - + ## Create the override # Search parent_cols = inst.users_collection child_cols = [child for pcol in parent_cols for child in pcol.children] - params = {'active_object': inst, 'selected_objects': [inst]} + params = {"active_object": inst, "selected_objects": [inst]} try: bpy.ops.object.make_override_library(params) ## check which collection is new in parents collection - asset_col = next((c for pcol in parent_cols for c in pcol.children if c not in child_cols), None) + asset_col = next( + (c for pcol in parent_cols for c in pcol.children if c not in child_cols), + None, + ) if not asset_col: - print('Overriden, but no collection found !!') + print("Overriden, but no collection found !!") return - + for ob in asset_col.all_objects: - if ob.type != 'ARMATURE': + if ob.type != "ARMATURE": continue if rig_pattern and not fnmatch(ob.name, rig_pattern): continue @@ -444,37 +488,40 @@ def load_col(filepath, name, link=True, override=True, rig_pattern=None, context return ob except Exception as e: - print(f'Override failed on {col.name}') + print(f"Override failed on {col.name}") print(e) - + return inst -def get_preview(asset_path='', asset_name=''): +def get_preview(asset_path="", asset_name=""): asset_preview_dir = Path(asset_path).parents[1] name = asset_name.lower() - return next((f for f in asset_preview_dir.rglob('*') if f.stem.lower().endswith(name)), None) + return next( + (f for f in asset_preview_dir.rglob("*") if f.stem.lower().endswith(name)), None + ) + def get_object_libraries(ob): if ob is None: return [] - + libraries = [ob.library] if ob.data: 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] - + filepaths = [] for l in libraries: if not l or not l.filepath: continue - + absolute_filepath = abspath(bpy.path.abspath(l.filepath, library=l)) if absolute_filepath in filepaths: continue filepaths.append(absolute_filepath) - return filepaths \ No newline at end of file + return filepaths diff --git a/common/catalog.py b/common/catalog.py index 3bdcd68..3107974 100644 --- a/common/catalog.py +++ b/common/catalog.py @@ -1,4 +1,3 @@ - from pathlib import Path import uuid import bpy @@ -6,6 +5,7 @@ import bpy class CatalogItem: """Represent a single item of a catalog""" + def __init__(self, catalog, path=None, name=None, id=None): self.catalog = catalog @@ -16,7 +16,7 @@ class CatalogItem: if isinstance(self.path, Path): self.path = self.path.as_posix() - + if self.path and not self.name: self.name = self.norm_name(self.path) @@ -25,10 +25,10 @@ class CatalogItem: def norm_name(self, name): """Get a norm name from a catalog_path entry""" - return name.replace('/', '-') + return name.replace("/", "-") 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: @@ -60,11 +60,12 @@ class CatalogContext: if self.active_item: return self.active_item.path - return '' + return "" class Catalog: """Represent the catalog of the blender asset browser library""" + def __init__(self, directory=None): self.directory = None @@ -72,14 +73,14 @@ class Catalog: if directory: self.directory = Path(directory) - + self.context = CatalogContext() - + @property def filepath(self): """Get the filepath of the catalog text file relative to the directory""" if self.directory: - return self.directory /'blender_assets.cats.txt' + return self.directory / "blender_assets.cats.txt" def read(self): """Read the catalog file of the library target directory or of the specified directory""" @@ -88,47 +89,49 @@ class Catalog: return {} self._data.clear() - - print(f'Read catalog from {self.filepath}') - for line in self.filepath.read_text(encoding="utf-8").split('\n'): - if line.startswith(('VERSION', '#')) or not line: + + print(f"Read catalog from {self.filepath}") + for line in self.filepath.read_text(encoding="utf-8").split("\n"): + if line.startswith(("VERSION", "#")) or not line: continue - cat_id, cat_path, cat_name = line.split(':') - self._data[cat_id] = CatalogItem(self, name=cat_name, id=cat_id, path=cat_path) - + cat_id, cat_path, cat_name = line.split(":") + self._data[cat_id] = CatalogItem( + self, name=cat_name, id=cat_id, path=cat_path + ) + return self def write(self, sort=True): """Write the catalog file in the library target directory or of the specified directory""" - - if not self.filepath: - raise Exception(f'Cannot write catalog {self} no filepath setted') - lines = ['VERSION 1', ''] + if not self.filepath: + raise Exception(f"Cannot write catalog {self} no filepath setted") + + lines = ["VERSION 1", ""] catalog_items = list(self) if sort: - catalog_items.sort(key=lambda x : x.path) + catalog_items.sort(key=lambda x: x.path) for catalog_item in catalog_items: lines.append(f"{catalog_item.id}:{catalog_item.path}:{catalog_item.name}") - - print(f'Write Catalog at: {self.filepath}') - self.filepath.write_text('\n'.join(lines), encoding="utf-8") + + print(f"Write Catalog at: {self.filepath}") + self.filepath.write_text("\n".join(lines), encoding="utf-8") def get(self, path=None, id=None, fallback=None): """Found a catalog item by is path or id""" if isinstance(path, Path): path = path.as_posix() - + if id: return self._data.get(id) for catalog_item in self: if catalog_item.path == path: return catalog_item - + return fallback def remove(self, catalog_item): @@ -140,7 +143,7 @@ class Catalog: if catalog_item: 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 def add(self, catalog_path): @@ -151,7 +154,7 @@ class Catalog: print(parent, self.get(parent)) if self.get(parent): continue - + cat_item = CatalogItem(self, path=parent) self._data[cat_item.id] = cat_item @@ -161,19 +164,23 @@ class Catalog: self._data[cat_item.id] = cat_item return cat_item - - def update(self, catalogs): - 'Add or remove catalog entries if on the list given or not' - catalogs = set(catalogs) # Remove doubles + def update(self, catalogs): + "Add or remove catalog entries if on the list given or not" + + catalogs = set(catalogs) # Remove doubles added = [c for c in catalogs if not self.get(path=c)] removed = [c.path for c in self if c.path not in catalogs] if added: - print(f'{len(added)} Catalog Entry Added \n{tuple(c.name for c in added[:10])}...\n') + print( + f"{len(added)} Catalog Entry Added \n{tuple(c.name for c in added[:10])}...\n" + ) if removed: - print(f'{len(removed)} Catalog Entry Removed \n{tuple(c.name for c in removed[:10])}...\n') + print( + f"{len(removed)} Catalog Entry Removed \n{tuple(c.name for c in removed[:10])}...\n" + ) for catalog_item in removed: self.remove(catalog_item) @@ -183,7 +190,7 @@ class Catalog: def __iter__(self): return self._data.values().__iter__() - + def __getitem__(self, key): if isinstance(key, int): return self._data.values()[key] @@ -191,10 +198,10 @@ class Catalog: return self._data[key] def __contains__(self, item): - if isinstance(item, str): # item is the id + if isinstance(item, str): # item is the id return item in self._data else: return item in self def __repr__(self): - return f'Catalog(filepath={self.filepath})' \ No newline at end of file + return f"Catalog(filepath={self.filepath})" diff --git a/common/file_utils.py b/common/file_utils.py index 3286202..8509d60 100644 --- a/common/file_utils.py +++ b/common/file_utils.py @@ -1,4 +1,3 @@ - """Generic python functions to make operation on file and names""" import fnmatch @@ -15,6 +14,7 @@ import shutil import contextlib + @contextlib.contextmanager def cd(path): """Changes working directory and returns to previous on exit.""" @@ -25,20 +25,24 @@ def cd(path): finally: os.chdir(prev_cwd) + def install_module(module_name, package_name=None): - '''Install a python module with pip or return it if already installed''' + """Install a python module with pip or return it if already installed""" try: module = importlib.import_module(module_name) except ModuleNotFoundError: - print(f'Installing Module {module_name} ....') + print(f"Installing Module {module_name} ....") + + subprocess.call([sys.executable, "-m", "ensurepip"]) + subprocess.call( + [sys.executable, "-m", "pip", "install", package_name or module_name] + ) - subprocess.call([sys.executable, '-m', 'ensurepip']) - subprocess.call([sys.executable, '-m', 'pip', 'install', package_name or module_name]) - module = importlib.import_module(module_name) - + return module + def import_module_from_path(path): from importlib import util @@ -46,50 +50,64 @@ def import_module_from_path(path): path = Path(path) spec = util.spec_from_file_location(path.stem, str(path)) mod = util.module_from_spec(spec) - + spec.loader.exec_module(mod) return mod except Exception as e: - print(f'Cannot import file {path}') + print(f"Cannot import file {path}") 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 = string.replace('_', ' ') - string = string.replace('-', ' ') - string = re.sub('[ ]+', ' ', string) - string = re.sub('[ ]+\/[ ]+', '/', string) + string = string.replace("_", " ") + string = string.replace("-", " ") + string = re.sub("[ ]+", " ", string) + string = re.sub("[ ]+\/[ ]+", "/", string) string = string.strip() if format: string = format(string) - + # Padd rightest number - string = re.sub(r'(\d+)(?!.*\d)', lambda x : x.group(1).zfill(padding), string) - - string = string.replace(' ', separator) - string = unicodedata.normalize('NFKD', string).encode('ASCII', 'ignore').decode("utf-8") + string = re.sub(r"(\d+)(?!.*\d)", lambda x: x.group(1).zfill(padding), string) + + string = string.replace(" ", separator) + string = ( + unicodedata.normalize("NFKD", string).encode("ASCII", "ignore").decode("utf-8") + ) return string + def remove_version(filepath): - pattern = '_v[0-9]+\.' + pattern = "_v[0-9]+\." search = re.search(pattern, filepath) if search: - filepath = filepath.replace(search.group()[:-1], '') + filepath = filepath.replace(search.group()[:-1], "") return Path(filepath).name + def is_exclude(name, patterns) -> bool: # from fnmatch import fnmatch - if not isinstance(patterns, (list,tuple)) : + if not isinstance(patterns, (list, tuple)): patterns = [patterns] return any([fnmatch(name, p) for p in patterns]) -def get_last_files(root, pattern=r'_v\d{3}\.\w+', only_matching=False, ex_file=None, ex_dir=None, keep=1, verbose=False) -> list: - '''Recursively get last(s) file(s) (when there is multiple versions) in passed directory + +def get_last_files( + root, + pattern=r"_v\d{3}\.\w+", + only_matching=False, + ex_file=None, + ex_dir=None, + keep=1, + verbose=False, +) -> list: + """Recursively get last(s) file(s) (when there is multiple versions) in passed directory root -> str: Filepath of the folder to scan. pattern -> str: Regex pattern to group files. only_matching -> bool: Discard files that aren't matched by regex pattern. @@ -97,7 +115,7 @@ def get_last_files(root, pattern=r'_v\d{3}\.\w+', only_matching=False, ex_file=N ex_dir -> list : List of fn_match pattern of directory name to skip. keep -> int: Number of lasts versions to keep when there are mutliple versionned files (e.g: 1 keep only last). verbose -> bool: Print infos in console. - ''' + """ files = [] if ex_file is None: @@ -111,7 +129,9 @@ def get_last_files(root, pattern=r'_v\d{3}\.\w+', only_matching=False, ex_file=N dirs = [f for f in all_items if f.is_dir()] - for i in range(len(allfiles)-1,-1,-1):# fastest way to iterate on index in reverse + for i in range( + len(allfiles) - 1, -1, -1 + ): # fastest way to iterate on index in reverse if not re.search(pattern, allfiles[i].name): if only_matching: allfiles.pop(i) @@ -119,7 +139,10 @@ def get_last_files(root, pattern=r'_v\d{3}\.\w+', only_matching=False, ex_file=N files.append(allfiles.pop(i).path) # separate remaining files in prefix grouped lists - lilist = [list(v) for k, v in groupby(allfiles, key=lambda x: re.split(pattern, x.name)[0])] + lilist = [ + list(v) + for k, v in groupby(allfiles, key=lambda x: re.split(pattern, x.name)[0]) + ] # get only item last of each sorted grouplist for l in lilist: @@ -128,17 +151,26 @@ def get_last_files(root, pattern=r'_v\d{3}\.\w+', only_matching=False, ex_file=N files.append(f.path) if verbose and len(l) > 1: - print(f'{root}: keep {str([x.name for x in versions])} out of {len(l)} elements') + print( + f"{root}: keep {str([x.name for x in versions])} out of {len(l)} elements" + ) - for d in dirs: # recursively treat all detected directory + for d in dirs: # recursively treat all detected directory if ex_dir and is_exclude(d.name, ex_dir): - # skip folder with excluded name + # skip folder with excluded name continue files += get_last_files( - d.path, pattern=pattern, only_matching=only_matching, ex_file=ex_file, ex_dir=ex_dir, keep=keep) + d.path, + pattern=pattern, + only_matching=only_matching, + ex_file=ex_file, + ex_dir=ex_dir, + keep=keep, + ) return sorted(files) + def copy_file(src, dst, only_new=False, only_recent=False): if dst.exists(): if only_new: @@ -147,19 +179,20 @@ def copy_file(src, dst, only_new=False, only_recent=False): return dst.parent.mkdir(exist_ok=True, parents=True) - print(f'Copy file from {src} to {dst}') - if platform.system() == 'Windows': - subprocess.call(['copy', str(src), str(dst)], shell=True) + print(f"Copy file from {src} to {dst}") + if platform.system() == "Windows": + subprocess.call(["copy", str(src), str(dst)], shell=True) else: - subprocess.call(['cp', str(src), str(dst)]) + 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) if includes: - includes = r'|'.join([fnmatch.translate(x) for x in includes]) + includes = r"|".join([fnmatch.translate(x) for x in includes]) 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(): dst.mkdir(exist_ok=True, parents=True) @@ -170,149 +203,157 @@ 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) elif src.is_dir(): - src_files = list(src.rglob('*')) + src_files = list(src.rglob("*")) if excludes: src_files = [f for f in src_files if not re.match(excludes, f.name)] if includes: src_files = [f for f in src_files if re.match(includes, f.name)] - dst_files = [dst/f.relative_to(src) for f in src_files] + dst_files = [dst / f.relative_to(src) for f in src_files] - for src_file, dst_file in zip(src_files, dst_files) : + for src_file, dst_file in zip(src_files, dst_files): if src_file.is_dir(): dst_file.mkdir(exist_ok=True, parents=True) else: - copy_file(src_file, dst_file, only_new=only_new, only_recent=only_recent) + copy_file( + src_file, dst_file, only_new=only_new, only_recent=only_recent + ) def open_file(filepath, select=False): - '''Open a filepath inside the os explorer''' - - if platform.system() == 'Darwin': # macOS - cmd = ['open'] - elif platform.system() == 'Windows': # Windows - cmd = ['explorer'] + """Open a filepath inside the os explorer""" + + if platform.system() == "Darwin": # macOS + cmd = ["open"] + elif platform.system() == "Windows": # Windows + cmd = ["explorer"] if select: - cmd += ['/select,'] - else: # linux variants - cmd = ['xdg-open'] + cmd += ["/select,"] + else: # linux variants + cmd = ["xdg-open"] if select: - cmd = ['nemo'] - + cmd = ["nemo"] + cmd += [str(filepath)] subprocess.Popen(cmd) + def open_blender_file(filepath=None): filepath = filepath or bpy.data.filepath - + cmd = sys.argv - + # if no filepath, use command as is to reopen blender - if filepath != '': - if len(cmd) > 1 and cmd[1].endswith('.blend'): + if filepath != "": + if len(cmd) > 1 and cmd[1].endswith(".blend"): cmd[1] = str(filepath) else: cmd.insert(1, str(filepath)) subprocess.Popen(cmd) -def read_file(path): - '''Read a file with an extension in (json, yaml, yml, txt)''' - exts = ('.json', '.yaml', '.yml', '.txt') +def read_file(path): + """Read a file with an extension in (json, yaml, yml, txt)""" + + exts = (".json", ".yaml", ".yml", ".txt") if not path: - print('Try to read empty file') - + print("Try to read empty file") + path = Path(path) if not path.exists(): - print('File not exist', path) + print("File not exist", path) return - + 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 txt = path.read_text() data = None - if path.suffix.lower() in ('.yaml', '.yml'): - yaml = install_module('yaml') + if path.suffix.lower() in (".yaml", ".yml"): + yaml = install_module("yaml") try: data = yaml.safe_load(txt) except Exception: - print(f'Could not load yaml file {path}') + print(f"Could not load yaml file {path}") return - elif path.suffix.lower() == '.json': + elif path.suffix.lower() == ".json": try: data = json.loads(txt) except Exception: - print(f'Could not load json file {path}') + print(f"Could not load json file {path}") return else: data = txt return data -def write_file(path, data, indent=4): - '''Read a file with an extension in (json, yaml, yml, text)''' - exts = ('.json', '.yaml', '.yml', '.txt') +def write_file(path, data, indent=4): + """Read a file with an extension in (json, yaml, yml, text)""" + + exts = (".json", ".yaml", ".yml", ".txt") if not path: - print('Try to write empty file') - + print("Try to write empty file") + path = Path(path) path.parent.mkdir(parents=True, exist_ok=True) if path.suffix not in exts: - print(f'Cannot read file {path}, extension must be in {exts}') + print(f"Cannot read file {path}, extension must be in {exts}") return - if path.suffix.lower() in ('.yaml', '.yml'): - yaml = install_module('yaml') + if path.suffix.lower() in (".yaml", ".yml"): + yaml = install_module("yaml") try: - path.write_text(yaml.dump(data), encoding='utf8') + path.write_text(yaml.dump(data), encoding="utf8") except Exception as e: print(e) - print(f'Could not write yaml file {path}') + print(f"Could not write yaml file {path}") return - elif path.suffix.lower() == '.json': + elif path.suffix.lower() == ".json": 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: print(e) - print(f'Could not write json file {path}') + print(f"Could not write json file {path}") return 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): - #actionlib_dir = get_actionlib_dir(custom=custom) - #local_actionlib_dir = get_actionlib_dir(local=True, custom=custom) + # actionlib_dir = get_actionlib_dir(custom=custom) + # local_actionlib_dir = get_actionlib_dir(local=True, custom=custom) try: if clear and Path(dst).exists(): shutil.rmtree(dst) - #set_actionlib_dir(custom=custom) + # set_actionlib_dir(custom=custom) - script = Path(__file__).parent / 'synchronize.py' + script = Path(__file__).parent / "synchronize.py" cmd = [ sys.executable, script, - '--src', str(src), - '--dst', str(dst), - '--only-new', json.dumps(only_new), - '--only-recent', json.dumps(only_recent), + "--src", + str(src), + "--dst", + str(dst), + "--only-new", + json.dumps(only_new), + "--only-recent", + json.dumps(only_recent), ] subprocess.Popen(cmd) except Exception as e: print(e) - \ No newline at end of file diff --git a/common/functions.py b/common/functions.py index bdff6a9..1766473 100644 --- a/common/functions.py +++ b/common/functions.py @@ -11,7 +11,8 @@ import os import re import time -#from asset_library.constants import ASSETLIB_FILENAME + +# 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 @@ -21,34 +22,37 @@ import bpy def command(func): - '''Decorator to be used from printed functions argument and run time''' - func_name = func.__name__.replace('_', ' ').title() - + """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 ---') + 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) ---') + print( + f"[>-] {func_name} --- Finished (total time : {time.time() - t0:.2f}s) ---" + ) return result - - return _command + + return _command + def asset_warning_callback(self, context): """Callback function to display a warning message when ading or modifying an asset""" - self.warning = '' + self.warning = "" if not self.name: - self.warning = 'You need to specify a name' + self.warning = "You need to specify a name" return if not self.catalog: - self.warning = 'You need to specify a catalog' + self.warning = "You need to specify a catalog" return lib = get_active_library() @@ -60,30 +64,33 @@ def asset_warning_callback(self, context): 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' + 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''' + """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 + # 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''' + """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_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 '' + return "" + """ def norm_asset_datas(asset_file_datas): @@ -181,7 +188,7 @@ def get_asset_source(replace_local=False): return source_path """ -''' +""" def get_catalog_path(filepath=None): filepath = filepath or bpy.data.filepath filepath = Path(filepath) @@ -196,7 +203,7 @@ def get_catalog_path(filepath=None): catalog.touch(exist_ok=False) return catalog -''' +""" # def read_catalog(path, key='path'): # cat_data = {} @@ -218,7 +225,7 @@ def get_catalog_path(filepath=None): # 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): @@ -302,27 +309,29 @@ def create_catalog_file(json_path : str|Path, keep_existing_category : bool = Tr return """ + def clear_env_libraries(): - print('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') + 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()] @@ -331,16 +340,17 @@ def clear_env_libraries(): 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 + +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) + # print('Read', path) library_data = read_file(path) clear_env_libraries() @@ -359,7 +369,8 @@ def set_env_libraries(path=None) -> list: return libs -''' + +""" def get_env_libraries(): env_libraries = {} @@ -391,21 +402,17 @@ def get_env_libraries(): } 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 + first_interval=waiting_time, ) - -''' +""" def set_assetlib_paths(): prefs = bpy.context.preferences @@ -452,16 +459,4 @@ def set_actionlib_paths(): prefs.filepaths.asset_libraries[lib_id].name = actionlib_name #prefs.filepaths.asset_libraries[lib_id].path = str(actionlib_dir) - ''' - - - - - - - - - - - - + """ diff --git a/common/library_cache.py b/common/library_cache.py index b8de562..d2f6ccd 100644 --- a/common/library_cache.py +++ b/common/library_cache.py @@ -10,7 +10,7 @@ class AssetCache: def __init__(self, file_cache, data=None): self.file_cache = file_cache - + self.catalog = None self.author = None self.description = None @@ -32,10 +32,7 @@ class AssetCache: @property def metadata(self): - metadata = { - '.library_id': self.library_id, - '.filepath': self.filepath - } + metadata = {".library_id": self.library_id, ".filepath": self.filepath} metadata.update(self._metadata) @@ -43,23 +40,23 @@ class AssetCache: @property def norm_name(self): - return self.name.replace(' ', '_').lower() + return self.name.replace(" ", "_").lower() def unique_name(self): return (self.filepath / self.name).as_posix() def set_data(self, data): - catalog = data['catalog'] + catalog = data["catalog"] if isinstance(catalog, (list, tuple)): - catalog = '/'.join(catalog) + 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', {}) + 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( @@ -69,11 +66,11 @@ class AssetCache: description=self.description, tags=self.tags, type=self.type, - name=self.name + name=self.name, ) def __repr__(self): - return f'AssetCache(name={self.name}, catalog={self.catalog})' + return f"AssetCache(name={self.name}, catalog={self.catalog})" def __eq__(self, other): return self.to_dict() == other.to_dict() @@ -81,7 +78,7 @@ class AssetCache: class AssetsCache: def __init__(self, file_cache): - + self.file_cache = file_cache self._data = [] @@ -111,12 +108,12 @@ class AssetsCache: return next((a for a in self if a.name == name), None) def __repr__(self): - return f'AssetsCache({list(self)})' + return f"AssetsCache({list(self)})" class FileCache: def __init__(self, library_cache, data=None): - + self.library_cache = library_cache self.filepath = None @@ -132,15 +129,15 @@ class FileCache: def set_data(self, data): - if 'filepath' in data: - self.filepath = Path(data['filepath']) + if "filepath" in data: + self.filepath = Path(data["filepath"]) - self.modified = data.get('modified', time.time_ns()) + self.modified = data.get("modified", time.time_ns()) - if data.get('type') == 'FILE': + if data.get("type") == "FILE": self.assets.add(data) - for asset_cache_data in data.get('assets', []): + for asset_cache_data in data.get("assets", []): self.assets.add(asset_cache_data) def to_dict(self): @@ -148,7 +145,7 @@ class FileCache: filepath=self.filepath.as_posix(), modified=self.modified, library_id=self.library_id, - assets=[asset_cache.to_dict() for asset_cache in self] + assets=[asset_cache.to_dict() for asset_cache in self], ) def __iter__(self): @@ -158,14 +155,14 @@ class FileCache: return self._data[key] def __repr__(self): - return f'FileCache(filepath={self.filepath})' + 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.filepath = data['filepath'] self.operation = operation self.asset_cache = asset_cache @@ -189,32 +186,49 @@ class LibraryCacheDiff: 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') + 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} + 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') + 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') + 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') + 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') - + 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') + print("No change in the library") return self def group_by(self, key): - '''Return groups of file cache diff using the key provided''' + """Return groups of file cache diff using the key provided""" data = list(self).sort(key=key) return groupby(data, key=key) @@ -228,17 +242,17 @@ class LibraryCacheDiff: return len(self._data) def __repr__(self): - return f'LibraryCacheDiff(operations={[o for o in self][:2]}...)' + 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): + def from_library(cls, library): filepath = library.library_path / f"blender_assets.{library.id}.json" return cls(filepath) @@ -248,28 +262,28 @@ class LibraryCache: @property def library_id(self): - return self.filepath.stem.split('.')[-1] + return self.filepath.stem.split(".")[-1] - #@property - #def filepath(self): + # @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 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}') - + print(f"Read cache from {self.filepath}") + for file_cache_data in read_file(self.filepath): self.add(file_cache_data) @@ -280,7 +294,7 @@ class LibraryCache: if tmp: filepath = self.tmp_filepath - print(f'Write cache file to {filepath}') + print(f"Write cache file to {filepath}") write_file(filepath, self._data) return filepath @@ -293,7 +307,7 @@ class LibraryCache: def add_asset_cache(self, asset_cache_data, filepath=None): if filepath is None: - filepath = asset_cache_data['filepath'] + filepath = asset_cache_data["filepath"] file_cache = self.get(filepath) if not file_cache: @@ -334,28 +348,28 @@ class LibraryCache: if new_cache is None: new_cache = self - return LibraryCacheDiff(old_cache, new_cache) + return LibraryCacheDiff(old_cache, new_cache) def update(self, cache_diff): - #Update the cache with the operations + # 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}' ) + 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}' ) + print(f"Asset {asset_cache_diff.name} not in file_cache {file_cache}") continue - if asset_cache_diff.operation == 'REMOVE': + if asset_cache_diff.operation == "REMOVE": file_cache.assets.remove(asset_cache_diff.name) - elif asset_cache_diff.operation in ('MODIFY', 'ADD'): + elif asset_cache_diff.operation in ("MODIFY", "ADD"): asset_cache.set_data(asset_cache_diff.asset_cache.to_dict()) - + return self def __len__(self): @@ -363,7 +377,7 @@ class LibraryCache: def __iter__(self): return iter(self._data) - + def __getitem__(self, key): if isinstance(key, str): return self.to_dict()[key] @@ -377,5 +391,4 @@ class LibraryCache: return next((a for a in self if a.filepath == filepath), None) def __repr__(self): - return f'LibraryCache(library_id={self.library_id})' - + return f"LibraryCache(library_id={self.library_id})" diff --git a/common/synchronize.py b/common/synchronize.py index 9be0551..0a0ec8d 100644 --- a/common/synchronize.py +++ b/common/synchronize.py @@ -1,4 +1,3 @@ - import argparse import fnmatch import importlib.util @@ -11,7 +10,7 @@ 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", Path(__file__).parent / "file_utils.py" ) utils = importlib.util.module_from_spec(spec) spec.loader.exec_module(utils) @@ -19,24 +18,29 @@ spec.loader.exec_module(utils) def synchronize(src, dst, only_new=False, only_recent=False): - excludes=['*.sync-conflict-*', '.*'] - includes=['*.blend', 'blender_assets.cats.txt'] - + 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 + 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) +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') + 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)) diff --git a/common/template.py b/common/template.py index 59cdce4..d7e1ebc 100644 --- a/common/template.py +++ b/common/template.py @@ -9,51 +9,52 @@ import string class TemplateFormatter(string.Formatter): def format_field(self, value, format_spec): if isinstance(value, str): - spec, sep = [*format_spec.split(':'), None][:2] - + spec, sep = [*format_spec.split(":"), None][:2] + if sep: - value = value.replace('_', ' ') - value = value = re.sub(r'([a-z])([A-Z])', rf'\1{sep}\2', value) - value = value.replace(' ', sep) - - if spec == 'u': + value = value.replace("_", " ") + value = value = re.sub(r"([a-z])([A-Z])", rf"\1{sep}\2", value) + value = value.replace(" ", sep) + + if spec == "u": value = value.upper() - elif spec == 'l': + elif spec == "l": value = value.lower() - elif spec == 't': + elif spec == "t": value = value.title() return super().format(value, format_spec) + class Template: - field_pattern = re.compile(r'{(\w+)\*{0,2}}') - field_pattern_recursive = re.compile(r'{(\w+)\*{2}}') + field_pattern = re.compile(r"{(\w+)\*{0,2}}") + field_pattern_recursive = re.compile(r"{(\w+)\*{2}}") def __init__(self, template): - #asset_data_path = Path(lib_path) / ASSETLIB_FILENAME + # asset_data_path = Path(lib_path) / ASSETLIB_FILENAME self.raw = template self.formatter = TemplateFormatter() @property def glob_pattern(self): - pattern = self.field_pattern_recursive.sub('**', self.raw) - pattern = self.field_pattern.sub('*', pattern) + pattern = self.field_pattern_recursive.sub("**", self.raw) + pattern = self.field_pattern.sub("*", pattern) return pattern @property def re_pattern(self): - pattern = self.field_pattern_recursive.sub('([\\\w -_.\/]+)', self.raw) - pattern = self.field_pattern.sub('([\\\w -_.]+)', pattern) - pattern = pattern.replace('?', '.') - pattern = pattern.replace('*', '.*') + pattern = self.field_pattern_recursive.sub("([\\\w -_.\/]+)", self.raw) + pattern = self.field_pattern.sub("([\\\w -_.]+)", pattern) + pattern = pattern.replace("?", ".") + pattern = pattern.replace("*", ".*") return re.compile(pattern) @property def fields(self): return self.field_pattern.findall(self.raw) - #return [f or '0' for f in fields] + # return [f or '0' for f in fields] def parse(self, path): @@ -61,7 +62,7 @@ class Template: res = self.re_pattern.findall(path) if not res: - print('Could not parse {path} with {self.re_pattern}') + print("Could not parse {path} with {self.re_pattern}") return {} fields = self.fields @@ -71,7 +72,7 @@ class Template: else: field_values = res[0] - return {k:v for k,v in zip(fields, field_values)} + return {k: v for k, v in zip(fields, field_values)} def norm_data(self, data): norm_data = {} @@ -81,7 +82,7 @@ class Template: v = v.as_posix() norm_data[k] = v - + return norm_data def format(self, data=None, **kargs): @@ -89,17 +90,17 @@ class Template: data = {**(data or {}), **kargs} try: - #print('FORMAT', self.raw, data) + # print('FORMAT', self.raw, data) path = self.formatter.format(self.raw, **self.norm_data(data)) 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 path = os.path.expandvars(path) return Path(path) def glob(self, directory, pattern=None): - '''If pattern is given it need to be absolute''' + """If pattern is given it need to be absolute""" if pattern is None: pattern = Path(directory, self.glob_pattern).as_posix() @@ -114,14 +115,14 @@ class Template: pattern = self.format(data, **kargs) 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 paths = glob(pattern.as_posix()) if paths: return Path(paths[0]) - - #return pattern + + # return pattern def __repr__(self): - return f'Template({self.raw})' \ No newline at end of file + return f"Template({self.raw})" diff --git a/constants.py b/constants.py index afc57c2..ef146b0 100644 --- a/constants.py +++ b/constants.py @@ -5,22 +5,23 @@ import bpy DATA_TYPE_ITEMS = [ ("ACTION", "Action", "", "ACTION", 0), ("COLLECTION", "Collection", "", "OUTLINER_OB_GROUP_INSTANCE", 1), - ("FILE", "File", "", "FILE", 2) + ("FILE", "File", "", "FILE", 2), ] DATA_TYPES = [i[0] for i in DATA_TYPE_ITEMS] -ICONS = {identifier: icon for identifier, name, description, icon, number in DATA_TYPE_ITEMS} +ICONS = { + identifier: icon for identifier, name, description, icon, number in DATA_TYPE_ITEMS +} ASSETLIB_FILENAME = "blender_assets.libs.json" MODULE_DIR = Path(__file__).parent -RESOURCES_DIR = MODULE_DIR / 'resources' +RESOURCES_DIR = MODULE_DIR / "resources" -LIBRARY_TYPE_DIR = MODULE_DIR / 'library_types' +LIBRARY_TYPE_DIR = MODULE_DIR / "library_types" LIBRARY_TYPES = [] -ADAPTER_DIR = MODULE_DIR / 'adapters' +ADAPTER_DIR = MODULE_DIR / "adapters" ADAPTERS = [] -PREVIEW_ASSETS_SCRIPT = MODULE_DIR / 'common' / 'preview_assets.py' - -#ADD_ASSET_DICT = {} +PREVIEW_ASSETS_SCRIPT = MODULE_DIR / "common" / "preview_assets.py" +# ADD_ASSET_DICT = {} diff --git a/file/__init__.py b/file/__init__.py index 1eed34c..d788645 100644 --- a/file/__init__.py +++ b/file/__init__.py @@ -1,8 +1,6 @@ +from asset_library.file import operators, gui, keymaps -from asset_library.file import ( - operators, gui, keymaps) - -if 'bpy' in locals(): +if "bpy" in locals(): import importlib importlib.reload(operators) @@ -11,10 +9,12 @@ if 'bpy' in locals(): import bpy + def register(): operators.register() keymaps.register() + def unregister(): operators.unregister() - keymaps.unregister() \ No newline at end of file + keymaps.unregister() diff --git a/file/bundle.py b/file/bundle.py index 39ff56c..9a14d0c 100644 --- a/file/bundle.py +++ b/file/bundle.py @@ -1,4 +1,3 @@ - import argparse import sys import json @@ -13,18 +12,22 @@ 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): +def bundle_library( + source_directory, + bundle_directory, + template_info, + thumbnail_template, + template=None, + data_file=None, +): - field_pattern = r'{(\w+)}' + 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('?', '.') + 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) @@ -33,65 +36,68 @@ def bundle_library(source_directory, bundle_directory, template_info, thumbnail_ 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)} + field_data = {k: v for k, v in zip(field_names, field_values)} - name = field_data.get('name', f.stem) + 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] + 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()} + "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}') + 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) + # 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...') + # print('Bundle Blend...') filepath = Path(filepath) - #asset_data_path = get_asset_datas_file(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') + 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') + 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} + # 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]) + 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 + # progress = 0 total_assets = len(asset_file_data) i = 0 @@ -99,63 +105,63 @@ def bundle_blend(filepath, depth=0): 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') + 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)}') + print(f"Progress: {int(i / total_assets * 100)}") - col = bpy.data.collections.new(name=asset_data['name']) + col = bpy.data.collections.new(name=asset_data["name"]) # Seems slow - #bpy.context.scene.collection.children.link(col) + # 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'] - ) + bpy.ops.ed.lib_id_load_custom_preview(filepath=asset_data["preview"]) - col.asset_data.description = asset_data.get('description', '') + col.asset_data.description = asset_data.get("description", "") - catalog_name = asset_data['catalog'] + catalog_name = asset_data["catalog"] catalog = catalog_data.get(catalog_name) if not catalog: - catalog = {'id': str(uuid.uuid4()), 'name': catalog_name} + catalog = {"id": str(uuid.uuid4()), "name": catalog_name} catalog_data[catalog_name] = catalog - col.asset_data.catalog_id = catalog['id'] + col.asset_data.catalog_id = catalog["id"] - for k, v in asset_data.get('metadata', {}).items(): + for k, v in asset_data.get("metadata", {}).items(): col.asset_data[k] = v - + i += 1 - print(f'Saving Blend to {blend_path}') + 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) +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) + 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:]] + if "--" in sys.argv: + index = sys.argv.index("--") + sys.argv = [sys.argv[index - 1], *sys.argv[index + 1 :]] args = parser.parse_args() @@ -165,6 +171,7 @@ if __name__ == '__main__' : template_info=args.template_info, thumbnail_template=args.thumbnail_template, template=args.template, - data_file=args.data_file) + data_file=args.data_file, + ) - bundle_blend(filepath=args.bundle_directory, depth=args.depth) \ No newline at end of file + bundle_blend(filepath=args.bundle_directory, depth=args.depth) diff --git a/file/gui.py b/file/gui.py index 27b00ac..2e6b94d 100644 --- a/file/gui.py +++ b/file/gui.py @@ -1,4 +1,3 @@ - import bpy from pathlib import Path @@ -18,21 +17,23 @@ from asset_library.common.functions import get_active_library def draw_context_menu(layout): - #asset = context.active_file + # asset = context.active_file layout.operator_context = "INVOKE_DEFAULT" lib = get_active_library() filepath = lib.library_type.get_active_asset_path() - layout.operator("assetlib.open_blend_file", text="Open Blend File")#.filepath = asset.asset_data['filepath'] + layout.operator( + "assetlib.open_blend_file", text="Open Blend File" + ) # .filepath = asset.asset_data['filepath'] op = layout.operator("wm.link", text="Link") op.filepath = str(filepath) - + op = layout.operator("wm.append", text="Append") op.filepath = str(filepath) def draw_header(layout): - '''Draw the header of the Asset Browser Window''' + """Draw the header of the Asset Browser Window""" layout.separator() - #layout.operator("actionlib.store_anim_pose", text='Add Action', icon='FILE_NEW') \ No newline at end of file + # layout.operator("actionlib.store_anim_pose", text='Add Action', icon='FILE_NEW') diff --git a/file/keymaps.py b/file/keymaps.py index 475c2da..ee477b3 100644 --- a/file/keymaps.py +++ b/file/keymaps.py @@ -1,5 +1,3 @@ - - from typing import List, Tuple import bpy @@ -7,13 +5,16 @@ from bpy.app.handlers import persistent addon_keymaps: List[Tuple[bpy.types.KeyMap, bpy.types.KeyMapItem]] = [] + def register() -> None: wm = bpy.context.window_manager if not wm.keyconfigs.addon: # This happens when Blender is running in the background. return - km = wm.keyconfigs.addon.keymaps.new(name="File Browser Main", space_type="FILE_BROWSER") + km = wm.keyconfigs.addon.keymaps.new( + name="File Browser Main", space_type="FILE_BROWSER" + ) kmi = km.keymap_items.new("assetlib.open_blend_file", "LEFTMOUSE", "DOUBLE_CLICK") addon_keymaps.append((km, kmi)) @@ -22,4 +23,4 @@ def register() -> None: def unregister() -> None: for km, kmi in addon_keymaps: km.keymap_items.remove(kmi) - addon_keymaps.clear() \ No newline at end of file + addon_keymaps.clear() diff --git a/file/operators.py b/file/operators.py index 1acff6f..21d2925 100644 --- a/file/operators.py +++ b/file/operators.py @@ -1,12 +1,14 @@ - import bpy from bpy.types import Context, Operator from bpy_extras import asset_utils from bpy.props import StringProperty from typing import List, Tuple, Set -from asset_library.common.file_utils import (open_blender_file, - synchronize, open_blender_file) +from asset_library.common.file_utils import ( + open_blender_file, + synchronize, + open_blender_file, +) from asset_library.common.functions import get_active_library @@ -14,44 +16,44 @@ from asset_library.common.functions import get_active_library class ASSETLIB_OT_open_blend_file(Operator): bl_idname = "assetlib.open_blend_file" bl_options = {"REGISTER", "UNDO"} - bl_label = 'Open Blender File' - bl_description = 'Open blender file' + bl_label = "Open Blender File" + bl_description = "Open blender file" @classmethod def poll(cls, context: Context) -> bool: if not asset_utils.SpaceAssetInfo.is_asset_browser(context.space_data): cls.poll_message_set("Current editor is not an asset browser") return False - + lib = get_active_library() - if not lib or lib.data_type != 'FILE': + if not lib or lib.data_type != "FILE": 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") return False - + return True def execute(self, context: Context) -> Set[str]: - + lib = get_active_library() filepath = lib.library_type.get_active_asset_path() open_blender_file(filepath) - return {'FINISHED'} + return {"FINISHED"} + + +classes = (ASSETLIB_OT_open_blend_file,) - -classes = ( - ASSETLIB_OT_open_blend_file, -) def register(): for cls in classes: bpy.utils.register_class(cls) + def unregister(): for cls in reversed(classes): - bpy.utils.unregister_class(cls) \ No newline at end of file + bpy.utils.unregister_class(cls) diff --git a/gui.py b/gui.py index 3d81c31..583a607 100644 --- a/gui.py +++ b/gui.py @@ -25,40 +25,36 @@ from asset_library.common.bl_utils import ( get_object_libraries, ) -from asset_library.common.functions import ( - get_active_library -) - +from asset_library.common.functions import get_active_library def pose_library_panel_poll(): - return bpy.context.object and bpy.context.object.mode == 'POSE' + 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' - ) + return bool(context.object and context.object.mode == "POSE") @classmethod def poll(cls, context: Context) -> bool: - return cls.pose_library_panel_poll(context); + 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' + bl_space_type = "VIEW_3D" + bl_region_type = "UI" + bl_category = "Item" @classmethod def poll(cls, context: Context) -> bool: @@ -70,9 +66,10 @@ class ASSETLIB_PT_libraries(Panel): 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 + 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" @@ -117,21 +114,23 @@ class ASSETLIB_PT_pose_library_usage(Panel): 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' +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'} + # 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'): + 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): @@ -144,34 +143,34 @@ class ASSETLIB_PT_pose_library_editing(PoseLibraryPanel, asset_utils.AssetBrowse 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') + 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): @@ -190,7 +189,7 @@ def is_option_region_visible(context, space): return False for region in context.area.regions: - if region.type == 'TOOL_PROPS' and region.width <= 1: + if region.type == "TOOL_PROPS" and region.width <= 1: return False return True @@ -209,10 +208,10 @@ def draw_assetbrowser_header(self, context): 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 + 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) @@ -224,7 +223,7 @@ def draw_assetbrowser_header(self, context): sub = row.row() sub.ui_units_x = 10 - sub.prop(params, "filter_search", text="", icon='VIEWZOOM') + sub.prop(params, "filter_search", text="", icon="VIEWZOOM") row.separator_spacer() @@ -239,17 +238,18 @@ def draw_assetbrowser_header(self, context): row.operator( "screen.region_toggle", text="", - icon='PREFERENCES', - depress=is_option_region_visible(context, space_data) - ).region_type = 'TOOL_PROPS' + 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'} + 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: @@ -257,6 +257,7 @@ def _on_asset_library_changed() -> None: area.tag_redraw() + def register_message_bus() -> None: bpy.msgbus.subscribe_rna( @@ -264,17 +265,20 @@ def register_message_bus() -> None: owner=_msgbus_owner, args=(), notify=_on_asset_library_changed, - options={'PERSISTENT'}, + 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. @@ -283,9 +287,9 @@ def _on_blendfile_load_post(none, other_none) -> None: classes = ( ASSETLIB_PT_pose_library_editing, - #ASSETLIB_PT_pose_library_usage, + # ASSETLIB_PT_pose_library_usage, ASSETLIB_MT_context_menu, - ASSETLIB_PT_libraries + ASSETLIB_PT_libraries, ) @@ -293,22 +297,26 @@ 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 + 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( + # 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) + # 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) + # bpy.types.ASSETBROWSER_MT_editor_menus.append(draw_assetbrowser_header) register_message_bus() bpy.app.handlers.load_pre.append(_on_blendfile_load_pre) @@ -319,15 +327,17 @@ 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 + 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 + # 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) + # bpy.types.ASSETBROWSER_MT_editor_menus.remove(draw_assetbrowser_header) diff --git a/keymaps.py b/keymaps.py index 27b875f..3048402 100644 --- a/keymaps.py +++ b/keymaps.py @@ -12,18 +12,25 @@ 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 = wm.keyconfigs.addon.keymaps.new( + name="File Browser Main", space_type="FILE_BROWSER" + ) - km_frames = wm.keyconfigs.user.keymaps.get('Frames') + km_frames = wm.keyconfigs.user.keymaps.get("Frames") if km_frames: - play = km_frames.keymap_items.get('screen.animation_play') + 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, - ) + "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)) @@ -33,10 +40,12 @@ def register() -> 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") + 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' + kmi.properties.name = "ASSETLIB_MT_context_menu" addon_keymaps.append((km, kmi)) kmi = km.keymap_items.new("assetlib.play_preview", "SPACE", "PRESS") @@ -45,14 +54,15 @@ def register() -> None: # 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]: + 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]: + 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() \ No newline at end of file + addon_keymaps.clear() diff --git a/library_types/__init__.py b/library_types/__init__.py index 9edfe9b..0c460f5 100644 --- a/library_types/__init__.py +++ b/library_types/__init__.py @@ -1,9 +1,8 @@ - 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(): +if "bpy" in locals(): import importlib importlib.reload(library_type) diff --git a/library_types/conform.py b/library_types/conform.py index dac0c95..4d509c9 100644 --- a/library_types/conform.py +++ b/library_types/conform.py @@ -1,15 +1,13 @@ - """ Plugin for making an asset library of all blender file found in a folder """ - from asset_library.library_types.scan_folder import ScanFolder from asset_library.common.bl_utils import load_datablocks from asset_library.common.template import Template import bpy -from bpy.props import (StringProperty, IntProperty, BoolProperty) +from bpy.props import StringProperty, IntProperty, BoolProperty import re from pathlib import Path from itertools import groupby @@ -24,27 +22,29 @@ from pprint import pprint class Conform(ScanFolder): name = "Conform" - source_directory : StringProperty(subtype='DIR_PATH') + source_directory: StringProperty(subtype="DIR_PATH") - target_template_file : StringProperty() - target_template_info : StringProperty() - target_template_image : StringProperty() - target_template_video : StringProperty() + target_template_file: StringProperty() + target_template_info: StringProperty() + target_template_image: StringProperty() + target_template_video: StringProperty() def draw_prefs(self, layout): layout.prop(self, "source_directory", text="Source : Directory") col = layout.column(align=True) - 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_video", icon='COPY_ID', text='Template video') - col.prop(self, "source_template_info", icon='COPY_ID', text='Template info') + 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_video", icon="COPY_ID", text="Template video") + col.prop(self, "source_template_info", icon="COPY_ID", text="Template info") col = layout.column(align=True) - col.prop(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_info", icon='COPY_ID', text='Template info') + col.prop( + 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_info", icon="COPY_ID", text="Template info") def get_asset_bundle_path(self, asset_data): """Template file are relative""" @@ -52,48 +52,50 @@ class Conform(ScanFolder): src_directory = Path(self.source_directory).resolve() 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) rel_path = asset_path.relative_to(src_directory).as_posix() 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 - #target_template_file = re.sub(r'{(\d+)}', r'{cat\1}', self.target_template_file) + # 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) 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 = Template(self.target_template_file).format(format_data, **field_data).with_suffix('.blend') + path = ( + Template(self.target_template_file) + .format(format_data, **field_data) + .with_suffix(".blend") + ) path = Path(self.bundle_directory, path).resolve() return path 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 if not image_template: return - asset_path = self.get_asset_bundle_path(asset_data) + asset_path = self.get_asset_bundle_path(asset_data) image_path = self.find_path(image_template, 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) - ) + bpy.ops.ed.lib_id_load_custom_preview(filepath=str(image_path)) 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: return asset.preview def generate_previews(self, cache_diff): - - print('Generate previews...') + + print("Generate previews...") # if cache in (None, ''): # cache = self.fetch() @@ -103,12 +105,10 @@ class Conform(ScanFolder): if isinstance(cache, (Path, str)): cache_diff = LibraryCacheDiff(cache_diff) - - - #TODO Support all multiple data_type + # TODO Support all multiple data_type 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) else: self.generate_asset_preview(asset_info) @@ -116,49 +116,55 @@ class Conform(ScanFolder): def generate_asset_preview(self, asset_info): """Only generate preview when conforming a library""" - #print('\ngenerate_preview', asset_info['filepath']) - + # print('\ngenerate_preview', asset_info['filepath']) + scn = bpy.context.scene vl = bpy.context.view_layer - #Creating the preview for collection, object or material - #camera = scn.camera + # Creating the preview for collection, object or material + # camera = scn.camera - data_type = self.data_type #asset_info['data_type'] - asset_path = self.format_path(asset_info['filepath']) + data_type = self.data_type # asset_info['data_type'] + asset_path = self.format_path(asset_info["filepath"]) # 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: - for asset_data in asset_info['assets']: + for asset_data in asset_info["assets"]: asset_data = dict(asset_data, filepath=asset_path) dst_asset_path = self.get_asset_bundle_path(asset_data) - dst_video_path = self.format_path(self.target_template_video, asset_data, filepath=dst_asset_path) + dst_video_path = self.format_path( + self.target_template_video, asset_data, filepath=dst_asset_path + ) 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 - + src_video_path = self.find_path(self.source_template_video, asset_data) 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) - + # Check if asset as a preview image or need it to be generated asset_data_names = {} 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) - name = asset_data['name'] + name = asset_data["name"] dst_asset_path = self.get_asset_bundle_path(asset_data) - dst_image_path = self.format_path(self.target_template_image, asset_data, filepath=dst_asset_path) + dst_image_path = self.format_path( + self.target_template_image, asset_data, filepath=dst_asset_path + ) 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 - + # Check if a source image exists and if so copying it in the new directory if self.source_template_image: - src_image_path = self.find_path(self.source_template_image, asset_data) + src_image_path = self.find_path( + self.source_template_image, asset_data + ) if src_image_path: if src_image_path.suffix == dst_image_path.suffix: @@ -170,47 +176,50 @@ class Conform(ScanFolder): continue - #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) - - if not asset_data_names:# No preview to generate + if not asset_data_names: # No preview to generate 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()) - assets = self.load_datablocks(asset_path, names=asset_names, link=True, type=data_type) + assets = self.load_datablocks( + asset_path, names=asset_names, link=True, type=data_type + ) for asset in assets: if not asset: continue asset_data = asset_data_names[asset.name] - image_path = asset_data['image_path'] + image_path = asset_data["image_path"] 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) continue - if data_type == 'COLLECTION': + if data_type == "COLLECTION": bpy.ops.object.collection_instance_add(name=asset.name) - + bpy.ops.view3d.camera_to_view_selected() instance = vl.objects.active - #scn.collection.children.link(asset) - + # scn.collection.children.link(asset) + 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) - #instance.user_clear() + # 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) \ No newline at end of file + bpy.ops.outliner.orphans_purge( + do_local_ids=True, do_linked_ids=True, do_recursive=True + ) diff --git a/library_types/copy_folder.py b/library_types/copy_folder.py index c016fb0..150a247 100644 --- a/library_types/copy_folder.py +++ b/library_types/copy_folder.py @@ -1,9 +1,7 @@ - """ 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 @@ -15,32 +13,29 @@ 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() + 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(',')] + 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) - 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'): + 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) \ No newline at end of file + # super().draw_prop(layout) diff --git a/library_types/kitsu.py b/library_types/kitsu.py index 68c2f25..58e06ac 100644 --- a/library_types/kitsu.py +++ b/library_types/kitsu.py @@ -1,15 +1,13 @@ - """ 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) +from bpy.props import StringProperty, IntProperty, BoolProperty import re from pathlib import Path from itertools import groupby @@ -25,73 +23,75 @@ 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() + 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') + 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') + 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') + print(f"Kitsu Url: {self.url} is empty") return url = self.url - if not url.endswith('/api'): - url += '/api' + if not url.endswith("/api"): + url += "/api" - print(f'Info: Setting Host for kitsu {url}') + 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') + print("Error: Kitsu Host is down") try: - print(f'Info: Log in to kitsu as {self.login}') + 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'] + return res["user"] except Exception as e: - print(f'Error: {traceback.format_exc()}') + 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']) + 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']) - ] + 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): @@ -100,37 +100,36 @@ class Kitsu(LibraryType): # 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''' + """Load an externalize image as preview for an asset using the source template""" - asset_path = self.format_path(Path(asset_data['filepath']).as_posix()) + 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) + 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) - ) + 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}') - + 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, ''): + 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 + # TODO Support all multiple data_type 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) else: self.generate_asset_preview(asset_info) @@ -141,35 +140,38 @@ class Kitsu(LibraryType): scn = bpy.context.scene vl = bpy.context.view_layer - asset_path = self.format_path(asset_info['filepath']) + asset_path = self.format_path(asset_info["filepath"]) lens = 85 if not asset_path.exists(): - print(f'Blend file {asset_path} not exit') + 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) + 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 + # 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}') + print(f"All previews already existing for {asset_path}") return - #asset_names = [a['name'] for a in asset_info['assets']] + # 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) + assets = self.load_datablocks( + asset_path, names=asset_names, link=True, type=data_type + ) print(asset_names) print(assets) @@ -177,93 +179,106 @@ class Kitsu(LibraryType): for asset in assets: if not asset: continue - - print(f'Generate Preview for asset {asset.name}') + + 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) - + + # 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': + 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.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.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}') + print(f"Render asset {asset.name} to {image_path}") bpy.ops.render.render(write_still=True) - #instance.user_clear() + # 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) + 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}') + print(f"Fetch Assets for {self.library.name}") - gazu = install_module('gazu') + 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} + 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_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) + 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}') + 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)}') + 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_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'], + 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'] + name=asset_data["name"], ) - + cache.add_asset_cache(asset_cache_data, filepath=asset_path) - return cache diff --git a/library_types/library_type.py b/library_types/library_type.py index bd5034a..2294501 100644 --- a/library_types/library_type.py +++ b/library_types/library_type.py @@ -1,11 +1,10 @@ - -#from asset_library.common.functions import (norm_asset_datas,) +# 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.constants import MODULE_DIR, RESOURCES_DIR -from asset_library import (action, collection, file) +from asset_library import action, collection, file from asset_library.common.library_cache import LibraryCacheDiff from bpy.types import PropertyGroup @@ -28,21 +27,21 @@ from copy import deepcopy class LibraryType(PropertyGroup): - #def __init__(self): + # def __init__(self): name = "Base Adapter" - #library = None - + # library = None + @property def library(self): prefs = self.addon_prefs for lib in prefs.libraries: if lib.library_type == self: return lib - + @property def bundle_directory(self): return self.library.library_path - + @property def data_type(self): return self.library.data_type @@ -78,24 +77,32 @@ class LibraryType(PropertyGroup): @property def addon_prefs(self): return get_addon_prefs() - + @property def module_type(self): lib_type = self.library.data_type - if lib_type == 'ACTION': + if lib_type == "ACTION": return action - elif lib_type == 'FILE': + elif lib_type == "FILE": return file - elif lib_type == 'COLLECTION': + elif lib_type == "COLLECTION": return collection @property def format_data(self): """Dict for formating template""" - return dict(self.to_dict(), bundle_dir=self.library.bundle_dir, parent=self.library.parent) + return dict( + self.to_dict(), + bundle_dir=self.library.bundle_dir, + parent=self.library.parent, + ) def to_dict(self): - return {p: getattr(self, p) for p in self.bl_rna.properties.keys() if p !='rna_type'} + return { + p: getattr(self, p) + for p in self.bl_rna.properties.keys() + if p != "rna_type" + } def read_catalog(self): return self.library.read_catalog() @@ -104,10 +111,10 @@ class LibraryType(PropertyGroup): return self.library.read_cache(filepath=filepath) def fetch(self): - raise Exception('This method need to be define in the library_type') + raise Exception("This method need to be define in the library_type") def norm_file_name(self, name): - return name.replace(' ', '_') + return name.replace(" ", "_") def read_file(self, file): return read_file(file) @@ -120,42 +127,46 @@ class LibraryType(PropertyGroup): dst = Path(destination) if not src.exists(): - print(f'Cannot copy file {src}: file not exist') + print(f"Cannot copy file {src}: file not exist") return dst.parent.mkdir(exist_ok=True, parents=True) - + 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 - print(f'Copy file from {src} to {dst}') + print(f"Copy file from {src} to {dst}") shutil.copy2(str(src), str(dst)) - def load_datablocks(self, src, names=None, type='objects', link=True, expr=None, assets_only=False): + def load_datablocks( + self, src, names=None, type="objects", link=True, expr=None, assets_only=False + ): """Link or append a datablock from a blendfile""" if type.isupper(): - type = f'{type.lower()}s' + type = f"{type.lower()}s" - return load_datablocks(src, names=names, type=type, link=link, expr=expr, assets_only=assets_only) + return load_datablocks( + src, names=names, type=type, link=link, expr=expr, assets_only=assets_only + ) def get_asset_data(self, asset): """Extract asset information on a datablock""" return dict( - name=asset.name, - type=asset.bl_rna.name.upper(), - author=asset.asset_data.author, - tags=list(asset.asset_data.tags.keys()), - metadata=dict(asset.asset_data), - description=asset.asset_data.description, - ) + name=asset.name, + type=asset.bl_rna.name.upper(), + author=asset.asset_data.author, + tags=list(asset.asset_data.tags.keys()), + metadata=dict(asset.asset_data), + description=asset.asset_data.description, + ) 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) - return Path(catalog, name, name).with_suffix('.blend') + return Path(catalog, name, name).with_suffix(".blend") def get_active_asset_library(self): prefs = get_addon_prefs() @@ -165,27 +176,27 @@ class LibraryType(PropertyGroup): return self lib = None - if '.library_id' in asset_handle.asset_data: - lib_id = asset_handle.asset_data['.library_id'] + if ".library_id" in asset_handle.asset_data: + lib_id = asset_handle.asset_data[".library_id"] lib = next((l for l in prefs.libraries if l.id == lib_id), None) if not lib: print(f"No library found for id {lib_id}") - + if not lib: lib = self - + return lib 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() asset_handle = bpy.context.asset_file_handle lib = self.get_active_asset_library() - if 'filepath' in asset_handle.asset_data: - asset_path = asset_handle.asset_data['filepath'] + if "filepath" in asset_handle.asset_data: + asset_path = asset_handle.asset_data["filepath"] asset_path = lib.library_type.format_path(asset_path) else: asset_path = bpy.types.AssetHandle.get_full_library_path( @@ -195,43 +206,43 @@ class LibraryType(PropertyGroup): return asset_path def generate_previews(self): - raise Exception('Need to be defined in the library_type') + raise Exception("Need to be defined in the library_type") 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 library_type") 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 library_type") 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 library_type") 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 library_type") 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 library_type") def format_asset_data(self, data): """Get a dict for use in template fields""" return { - 'asset_name': data['name'], - 'asset_path': Path(data['filepath']), - 'catalog': data['catalog'], - 'catalog_name': data['catalog'].replace('/', '_'), + "asset_name": data["name"], + "asset_path": Path(data["filepath"]), + "catalog": data["catalog"], + "catalog_name": data["catalog"].replace("/", "_"), } def format_path(self, template, data={}, **kargs): if not template: return None - + if data: data = self.format_asset_data(dict(data, **kargs)) else: data = kargs - - if template.startswith('.'): #the template is relative - template = Path(data['asset_path'], template).as_posix() + + if template.startswith("."): # the template is relative + template = Path(data["asset_path"], template).as_posix() params = dict( **data, @@ -261,13 +272,9 @@ class LibraryType(PropertyGroup): Path(asset_path).parent.mkdir(exist_ok=True, parents=True) 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): # """Read the catalog file of the library target directory or of the specified directory""" # catalog_path = self.get_catalog_path(directory) @@ -283,12 +290,12 @@ class LibraryType(PropertyGroup): # cat_id, cat_path, cat_name = line.split(':') # cat_data[cat_path] = {'id':cat_id, 'name':cat_name} - + # return cat_data # def write_catalog(self, catalog_data, directory=None): # """Write the catalog file in the library target directory or of the specified directory""" - + # catalog_path = self.get_catalog_path(directory) # lines = ['VERSION 1', ''] @@ -300,13 +307,13 @@ class LibraryType(PropertyGroup): # for p in Path(cat_path).parents[:-1]: # if p in cat_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: {catalog_path}') # catalog_path.write_text('\n'.join(lines), encoding="utf-8") @@ -321,27 +328,27 @@ class LibraryType(PropertyGroup): # return write_file(cache_path, list(asset_infos)) def prop_rel_path(self, path, prop): - '''Get a filepath relative to a property of the library_type''' - field_prop = '{%s}/'%prop + """Get a filepath relative to a property of the library_type""" + field_prop = "{%s}/" % prop prop_value = getattr(self, prop) prop_value = Path(os.path.expandvars(prop_value)).resolve() - + rel_path = Path(path).resolve().relative_to(prop_value).as_posix() return field_prop + rel_path def format_from_ext(self, ext): - if ext.startswith('.'): + if ext.startswith("."): ext = ext[1:] file_format = ext.upper() - if file_format == 'JPG': - file_format = 'JPEG' - elif file_format == 'EXR': - file_format = 'OPEN_EXR' - + if file_format == "JPG": + file_format = "JPEG" + elif file_format == "EXR": + file_format = "OPEN_EXR" + return file_format def save_image(self, image, filepath, remove=False): @@ -368,47 +375,54 @@ class LibraryType(PropertyGroup): filepath = Path(filepath) filepath.parent.mkdir(parents=True, exist_ok=True) - + img_size = preview.image_size px = [0] * img_size[0] * img_size[1] * 4 preview.image_pixels_float.foreach_get(px) - img = bpy.data.images.new(name=filepath.name, width=img_size[0], height=img_size[1], is_data=True, alpha=True) + img = bpy.data.images.new( + name=filepath.name, + width=img_size[0], + height=img_size[1], + is_data=True, + alpha=True, + ) img.pixels.foreach_set(px) self.save_image(img, filepath, remove=True) - def draw_header(self, layout): """Draw the header of the Asset Browser Window""" - #layout.separator() + # layout.separator() self.module_type.gui.draw_header(layout) def draw_context_menu(self, layout): - """Draw the context menu of the Asset Browser Window""" + """Draw the context menu of the Asset Browser Window""" self.module_type.gui.draw_context_menu(layout) def generate_blend_preview(self, asset_info): - asset_name = asset_info['name'] - catalog = asset_info['catalog'] + asset_name = asset_info["name"] + 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) if dst_image_path.exists(): return - + # 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: - src_image_path = self.get_template_path(src_image_path, asset_name, asset_path, catalog) + src_image_path = self.get_template_path( + src_image_path, asset_name, asset_path, catalog + ) if src_image_path and src_image_path.exists(): self.copy_file(src_image_path, dst_image_path) return - print(f'Thumbnailing {asset_path} to {dst_image_path}') - blender_thumbnailer = Path(bpy.app.binary_path).parent / 'blender-thumbnailer' + print(f"Thumbnailing {asset_path} to {dst_image_path}") + blender_thumbnailer = Path(bpy.app.binary_path).parent / "blender-thumbnailer" dst_image_path.parent.mkdir(exist_ok=True, parents=True) @@ -417,7 +431,7 @@ class LibraryType(PropertyGroup): success = dst_image_path.exists() 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)) return success @@ -532,14 +546,12 @@ class LibraryType(PropertyGroup): # def set_asset_catalog(self, asset, asset_data, catalog_data): # """Find the catalog if already exist or create it""" - # catalog_name = asset_data['catalog'] # catalog = catalog_data.get(catalog_name) # catalog_item = self.catalog.add(asset_data['catalog']) # asset.asset_data.catalog_id = catalog_item.id - # if not catalog: # catalog = {'id': str(uuid.uuid4()), 'name': catalog_name} # catalog_data[catalog_name] = catalog @@ -547,7 +559,7 @@ class LibraryType(PropertyGroup): # asset.asset_data.catalog_id = catalog['id'] def set_asset_metadata(self, asset, asset_cache): - """Create custom prop to an asset base on provided data""" + """Create custom prop to an asset base on provided data""" for k, v in asset_cache.metadata.items(): asset.asset_data[k] = v @@ -570,18 +582,22 @@ class LibraryType(PropertyGroup): """Get the bundle path for that asset""" catalog_parts = asset_cache.catalog_item.parts blend_name = asset_cache.norm_name - path_parts = catalog_parts[:self.library.blend_depth] + path_parts = catalog_parts[: self.library.blend_depth] - return Path(self.bundle_directory, *path_parts, blend_name, blend_name).with_suffix('.blend') + return Path( + self.bundle_directory, *path_parts, blend_name, blend_name + ).with_suffix(".blend") def bundle(self, cache_diff=None): """Group all new assets in one or multiple blends for the asset browser""" - supported_types = ('FILE', 'ACTION', 'COLLECTION') - supported_operations = ('ADD', 'REMOVE', 'MODIFY') + supported_types = ("FILE", "ACTION", "COLLECTION") + supported_operations = ("ADD", "REMOVE", "MODIFY") if self.data_type not in supported_types: - print(f'{self.data_type} is not supported yet supported types are {supported_types}') + print( + f"{self.data_type} is not supported yet supported types are {supported_types}" + ) return catalog = self.read_catalog() @@ -595,92 +611,99 @@ class LibraryType(PropertyGroup): # Write the cache in a temporary file for the generate preview script tmp_cache_file = cache.write(tmp=True) - bpy.ops.assetlib.generate_previews(name=self.library.name, cache=str(tmp_cache_file)) + bpy.ops.assetlib.generate_previews( + name=self.library.name, cache=str(tmp_cache_file) + ) elif isinstance(cache_diff, (Path, str)): - cache_diff = LibraryCacheDiff(cache_diff).read()#json.loads(Path(cache_diff).read_text(encoding='utf-8')) + cache_diff = LibraryCacheDiff( + cache_diff + ).read() # json.loads(Path(cache_diff).read_text(encoding='utf-8')) total_diffs = len(cache_diff) - print(f'Total Diffs={total_diffs}') + print(f"Total Diffs={total_diffs}") if total_diffs == 0: - print('No assets found') + print("No assets found") return i = 0 for bundle_path, asset_diffs in cache_diff.group_by(self.get_asset_bundle_path): 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)) 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) for asset_diff in asset_diffs: 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 asset_cache = asset_diff.asset_cache asset = getattr(bpy.data, self.data_types).get(asset_cache.name) - if operation == 'REMOVE': + if operation == "REMOVE": if asset: getattr(bpy.data, self.data_types).remove(asset) else: - print(f'ERROR : Remove Asset: {asset_cache.name} not found in {bundle_path}') + print( + f"ERROR : Remove Asset: {asset_cache.name} not found in {bundle_path}" + ) continue - elif operation == 'MODIFY': + elif operation == "MODIFY": if not asset: - print(f'WARNING: Modifiy Asset: {asset_cache.name} not found in {bundle_path} it will be created') + print( + 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: - #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") getattr(bpy.data, self.data_types).remove(asset) - #print(f"INFO: Add new asset: {asset_data['name']}") - asset = getattr(bpy.data, self.data_types).new(name=asset_cache.name) - + # print(f"INFO: Add new asset: {asset_data['name']}") + asset = getattr(bpy.data, self.data_types).new( + name=asset_cache.name + ) asset.asset_mark() self.set_asset_preview(asset, asset_cache) - #if not asset_preview: + # if not asset_preview: # assets_to_preview.append((asset_data['filepath'], asset_data['name'], asset_data['data_type'])) - #if self.externalize_data: + # if self.externalize_data: # self.write_preview(preview, filepath) - #self.set_asset_catalog(asset, asset_data['catalog']) + # self.set_asset_catalog(asset, asset_data['catalog']) asset.asset_data.catalog_id = catalog.add(asset_cache.catalog).id self.set_asset_metadata(asset, asset_cache) self.set_asset_tags(asset, asset_cache) self.set_asset_info(asset, asset_cache) - 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) bpy.ops.wm.save_as_mainfile(filepath=str(bundle_path), compress=True) - + if write_cache: cache.write() - #self.write_catalog(catalog_data) + # self.write_catalog(catalog_data) catalog.write() - bpy.ops.wm.quit_blender() - + # def unflatten_cache(self, cache): # """ Return a new unflattten list of asset data # grouped by filepath""" @@ -725,7 +748,7 @@ class LibraryType(PropertyGroup): # new_cache.append({**asset_info, **asset_data}) # else: # new_cache.append(asset_info) - + # return new_cache # def diff(self, asset_infos=None): @@ -736,7 +759,7 @@ class LibraryType(PropertyGroup): # if cache is None: # print(f'Fetch The library {self.library.name} for the first time, might be long...') # cache = [] - + # asset_infos = asset_infos or self.fetch() # cache = {f"{a['filepath']}/{a['name']}": a for a in self.flatten_cache(cache)} @@ -752,7 +775,7 @@ class LibraryType(PropertyGroup): # 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] @@ -769,4 +792,3 @@ class LibraryType(PropertyGroup): annotations = self.__class__.__annotations__ for k, v in annotations.items(): layout.prop(self, k, text=bpy.path.display_name(k)) - \ No newline at end of file diff --git a/library_types/poly_haven.py b/library_types/poly_haven.py index 074fb0f..11b8da4 100644 --- a/library_types/poly_haven.py +++ b/library_types/poly_haven.py @@ -1,15 +1,13 @@ - """ 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) +from bpy.props import StringProperty, IntProperty, BoolProperty, EnumProperty import re from pathlib import Path from itertools import groupby @@ -27,27 +25,34 @@ from pprint import pprint as pp REQ_HEADERS = requests.utils.default_headers() REQ_HEADERS.update({"User-Agent": "Blender: PH Assets"}) + class PolyHaven(LibraryType): name = "Poly Haven" # template_name : StringProperty() # template_file : StringProperty() - directory : StringProperty(subtype='DIR_PATH') - asset_type : EnumProperty(items=[(i.replace(' ', '_').upper(), i, '') for i in ('HDRIs', 'Models', 'Textures')], default='HDRIS') - main_category : StringProperty( - default='artificial light, natural light, nature, studio, skies, urban' + directory: StringProperty(subtype="DIR_PATH") + asset_type: EnumProperty( + items=[ + (i.replace(" ", "_").upper(), i, "") + for i in ("HDRIs", "Models", "Textures") + ], + default="HDRIS", ) - secondary_category : StringProperty( - default='high constrast, low constrast, medium constrast, midday, morning-afternoon, night, sunrise-sunset' + main_category: StringProperty( + default="artificial light, natural light, nature, studio, skies, urban" + ) + secondary_category: StringProperty( + default="high constrast, low constrast, medium constrast, midday, morning-afternoon, night, sunrise-sunset" ) - #blend_depth: IntProperty(default=1) + # blend_depth: IntProperty(default=1) # url: StringProperty() # login: StringProperty() # password: StringProperty(subtype='PASSWORD') # project_name: StringProperty() - + def get_asset_path(self, name, catalog, directory=None): # chemin: Source, Asset_type, asset_name / asset_name.blend -> PolyHaven/HDRIs/test/test.blend directory = directory or self.source_directory @@ -64,37 +69,40 @@ class PolyHaven(LibraryType): def format_asset_info(self, asset_info, asset_path): # prend un asset info et output un asset description - asset_path = self.prop_rel_path(asset_path, 'source_directory') - modified = asset_info.get('modified', time.time_ns()) + asset_path = self.prop_rel_path(asset_path, "source_directory") + modified = asset_info.get("modified", time.time_ns()) return dict( filepath=asset_path, modified=modified, library_id=self.library.id, - assets=[dict( - catalog=asset_data.get('catalog', asset_info['catalog']), - author=asset_data.get('author'), - metadata=asset_data.get('metadata', {}), - description=asset_data.get('description', ''), - tags=asset_data.get('tags', []), - type=self.data_type, - image=self.template_image, - video=self.template_video, - name=asset_data['name']) for asset_data in asset_info['assets'] - ] + assets=[ + dict( + catalog=asset_data.get("catalog", asset_info["catalog"]), + author=asset_data.get("author"), + metadata=asset_data.get("metadata", {}), + description=asset_data.get("description", ""), + tags=asset_data.get("tags", []), + type=self.data_type, + image=self.template_image, + video=self.template_video, + name=asset_data["name"], + ) + for asset_data in asset_info["assets"] + ], ) - + def fetch(self): """Gather in a list all assets found in the folder""" - print(f'Fetch Assets for {self.library.name}') - - print('self.asset_type: ', self.asset_type) + print(f"Fetch Assets for {self.library.name}") + + print("self.asset_type: ", self.asset_type) url = f"https://api.polyhaven.com/assets?t={self.asset_type.lower()}" # url2 = f"https://polyhaven.com/{self.asset_type.lower()}" # url += "&future=true" if early_access else "" # verify_ssl = not bpy.context.preferences.addons["polyhavenassets"].preferences.disable_ssl_verify - + verify_ssl = False try: res = requests.get(url, headers=REQ_HEADERS, verify=verify_ssl) @@ -108,34 +116,33 @@ class PolyHaven(LibraryType): error = f"Error retrieving asset list, status code: {res.status_code}" print(error) # return (error, None) - + catalog = None # return (None, res.json()) for asset_info in res.json().values(): main_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: main_category = category if category in self.secondary_category and not secondary_category: secondary_category = category - + if main_category and secondary_category: - catalog = f'{main_category}_{secondary_category}' + catalog = f"{main_category}_{secondary_category}" if not catalog: return - asset_path = self.get_asset_path(asset_info['name'], catalog) - print('asset_path: ', asset_path) + asset_path = self.get_asset_path(asset_info["name"], catalog) + print("asset_path: ", 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(res2.json()) # print(res2) - # return asset_infos diff --git a/library_types/scan_folder.py b/library_types/scan_folder.py index cd13567..9a36052 100644 --- a/library_types/scan_folder.py +++ b/library_types/scan_folder.py @@ -1,15 +1,13 @@ - """ 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) +from bpy.props import StringProperty, IntProperty, BoolProperty import re from pathlib import Path from itertools import groupby @@ -23,21 +21,21 @@ import time class ScanFolder(LibraryType): name = "Scan Folder" - source_directory : StringProperty(subtype='DIR_PATH') + source_directory: StringProperty(subtype="DIR_PATH") - source_template_file : StringProperty() - source_template_image : StringProperty() - source_template_video : StringProperty() - source_template_info : StringProperty() + source_template_file: StringProperty() + source_template_image: StringProperty() + source_template_video: StringProperty() + source_template_info: StringProperty() def draw_prefs(self, layout): layout.prop(self, "source_directory", text="Source: Directory") col = layout.column(align=True) - 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_video", icon='COPY_ID', text='Template video') - col.prop(self, "source_template_info", icon='COPY_ID', text='Template info') + 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_video", icon="COPY_ID", text="Template video") + col.prop(self, "source_template_info", icon="COPY_ID", text="Template info") def get_asset_path(self, name, catalog, directory=None): directory = directory or self.source_directory @@ -49,20 +47,26 @@ class ScanFolder(LibraryType): def get_image_path(self, name, catalog, filepath): catalog = self.norm_file_name(catalog) name = self.norm_file_name(name) - return self.format_path(self.source_template_image, dict(name=name, catalog=catalog, filepath=filepath)) + return self.format_path( + self.source_template_image, + dict(name=name, catalog=catalog, filepath=filepath), + ) def get_video_path(self, name, catalog, filepath): catalog = self.norm_file_name(catalog) name = self.norm_file_name(name) - return self.format_path(self.source_template_video, dict(name=name, catalog=catalog, filepath=filepath)) + return self.format_path( + self.source_template_video, + dict(name=name, catalog=catalog, filepath=filepath), + ) def new_asset(self, asset, asset_data): - raise Exception('Need to be defined in the library_type') + raise Exception("Need to be defined in the library_type") 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 library_type") - ''' + """ def format_asset_info(self, asset_datas, asset_path, modified=None): asset_path = self.prop_rel_path(asset_path, 'source_directory') @@ -97,38 +101,38 @@ class ScanFolder(LibraryType): name=asset_data['name']) for asset_data in asset_datas ] ) - ''' - + """ + 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) image_template = self.source_template_image if not image_template: return - - image_path = self.find_path(image_template, asset_cache.to_dict(), filepath=asset_path) + + image_path = self.find_path( + image_template, asset_cache.to_dict(), 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) - ) + bpy.ops.ed.lib_id_load_custom_preview(filepath=str(image_path)) 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: return asset.preview def bundle(self, cache_diff=None): """Group all new assets in one or multiple blends for the asset browser""" - if self.data_type not in ('FILE', 'ACTION', 'COLLECTION'): - print(f'{self.data_type} is not supported yet') + if self.data_type not in ("FILE", "ACTION", "COLLECTION"): + print(f"{self.data_type} is not supported yet") return - #catalog_data = self.read_catalog() + # catalog_data = self.read_catalog() catalog = self.read_catalog() cache = None @@ -140,59 +144,69 @@ class ScanFolder(LibraryType): # Write the cache in a temporary file for the generate preview script tmp_cache_file = cache.write(tmp=True) - bpy.ops.assetlib.generate_previews(name=self.library.name, cache=str(tmp_cache_file)) + bpy.ops.assetlib.generate_previews( + name=self.library.name, cache=str(tmp_cache_file) + ) elif isinstance(cache_diff, (Path, str)): - cache_diff = json.loads(Path(cache_diff).read_text(encoding='utf-8')) + cache_diff = json.loads(Path(cache_diff).read_text(encoding="utf-8")) 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) - print(f'total_assets={total_assets}') + print(f"total_assets={total_assets}") if total_assets == 0: - print('No assets found') + print("No assets found") return i = 0 - for blend_path, asset_cache_diffs in cache_diff.group_by(key=self.get_asset_bundle_path): + for blend_path, asset_cache_diffs in cache_diff.group_by( + key=self.get_asset_bundle_path + ): 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)) 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) for asset_cache_diff in asset_cache_diffs: 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 asset_cache = asset_cache_diff.asset_cache asset_name = asset_cache.name asset = getattr(bpy.data, self.data_types).get(asset_name) - if operation == 'REMOVE': + if operation == "REMOVE": if asset: getattr(bpy.data, self.data_types).remove(asset) else: - print(f'ERROR : Remove Asset: {asset_name} not found in {blend_path}') + print( + f"ERROR : Remove Asset: {asset_name} not found in {blend_path}" + ) continue - if asset_cache_diff.operation == 'MODIFY' and not asset: - print(f'WARNING: Modifiy Asset: {asset_name} not found in {blend_path} it will be created') + if asset_cache_diff.operation == "MODIFY" and not asset: + print( + 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: - #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") getattr(bpy.data, self.data_types).remove(asset) - #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) else: - print(f'operation {operation} not supported should be in (ADD, REMOVE, MODIFY)') + print( + f"operation {operation} not supported should be in (ADD, REMOVE, MODIFY)" + ) continue asset.asset_mark() @@ -202,14 +216,13 @@ class ScanFolder(LibraryType): self.set_asset_metadata(asset, asset_cache) self.set_asset_tags(asset, asset_cache) self.set_asset_info(asset, asset_cache) - + 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) 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 cache is None: @@ -220,82 +233,84 @@ class ScanFolder(LibraryType): catalog.update(cache.catalogs) catalog.write() - + bpy.ops.wm.quit_blender() def fetch(self): """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) template_file = Template(self.source_template_file) - #catalog_data = self.read_catalog(directory=source_directory) - #catalog_ids = {v['id']: k for k, v in catalog_data.items()} + # catalog_data = self.read_catalog(directory=source_directory) + # catalog_ids = {v['id']: k for k, v in catalog_data.items()} - #self.catalog.read() + # self.catalog.read() cache = self.read_cache() - print(f'Search for blend using glob template: {template_file.glob_pattern}') - print(f'Scanning Folder {source_directory}...') + print(f"Search for blend using glob template: {template_file.glob_pattern}") + print(f"Scanning Folder {source_directory}...") - #new_cache = LibraryCache() + # new_cache = LibraryCache() 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 # Check if the asset description as already been cached file_cache = next((a for a in cache if a.filepath == source_rel_path), None) if file_cache: - if file_cache.modified >= modified: #print(asset_path, 'is skipped because not modified') + if ( + file_cache.modified >= modified + ): # print(asset_path, 'is skipped because not modified') continue else: file_cache = cache.add(filepath=source_rel_path) - + rel_path = asset_path.relative_to(source_directory).as_posix() field_data = template_file.parse(rel_path) # Create the catalog path from the actual path of the asset - catalog = [v for k,v in sorted(field_data.items()) if re.findall('cat[0-9]+', k)] - #catalogs = [c.replace('_', ' ').title() for c in catalogs] + catalog = [ + v for k, v in sorted(field_data.items()) if re.findall("cat[0-9]+", k) + ] + # 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( - name=asset_name, - type='FILE', - catalog=catalog, - modified=modified + name=asset_name, type="FILE", catalog=catalog, modified=modified ) continue # Now check if there is a asset description file (Commented for now propably not usefull) - #asset_info_path = self.find_path(self.source_template_info, asset_info, filepath=asset_path) - #if asset_info_path: + # asset_info_path = self.find_path(self.source_template_info, asset_info, filepath=asset_path) + # if asset_info_path: # new_cache.append(self.read_file(asset_info_path)) # continue # Scan the blend file for assets inside - print(f'Scanning blendfile {asset_path}...') - assets = self.load_datablocks(asset_path, type=self.data_types, link=True, assets_only=True) - print(f'Found {len(assets)} {self.data_types} inside') + print(f"Scanning blendfile {asset_path}...") + assets = self.load_datablocks( + asset_path, type=self.data_types, link=True, assets_only=True + ) + print(f"Found {len(assets)} {self.data_types} inside") for asset in assets: - #catalog_path = catalog_ids.get(asset.asset_data.catalog_id) + # catalog_path = catalog_ids.get(asset.asset_data.catalog_id) - #if not catalog_path: + # if not catalog_path: # print(f'No catalog found for asset {asset.name}') - #catalog_path = asset_info['catalog']#asset_path.relative_to(self.source_directory).as_posix() - + # catalog_path = asset_info['catalog']#asset_path.relative_to(self.source_directory).as_posix() + # For now the catalog used is the one extract from the template file file_cache.assets.add(self.get_asset_data(asset), catalog=catalog) getattr(bpy.data, self.data_types).remove(asset) return cache - diff --git a/operators.py b/operators.py index 03b1681..03a5891 100644 --- a/operators.py +++ b/operators.py @@ -1,7 +1,6 @@ - - from typing import Set -#import shutil + +# import shutil from pathlib import Path import subprocess import importlib @@ -11,22 +10,19 @@ import json import bpy from bpy_extras import asset_utils from bpy.types import Context, Operator -from bpy.props import ( - BoolProperty, - EnumProperty, - StringProperty, - IntProperty) +from bpy.props import BoolProperty, EnumProperty, StringProperty, IntProperty -#from asset_library.constants import (DATA_TYPES, DATA_TYPE_ITEMS, MODULE_DIR) +# from asset_library.constants import (DATA_TYPES, DATA_TYPE_ITEMS, MODULE_DIR) import asset_library from asset_library.common.bl_utils import ( attr_set, - get_addon_prefs, - get_bl_cmd, + get_addon_prefs, + get_bl_cmd, get_view3d_persp, - #suitable_areas, + # suitable_areas, refresh_asset_browsers, - load_datablocks) + load_datablocks, +) from asset_library.common.file_utils import open_blender_file, synchronize from asset_library.common.functions import get_active_library, asset_warning_callback @@ -42,8 +38,8 @@ import bgl class ASSETLIB_OT_remove_assets(Operator): bl_idname = "assetlib.remove_assets" bl_options = {"REGISTER", "UNDO", "INTERNAL"} - bl_label = 'Remove Assets' - bl_description = 'Remove Selected Assets' + bl_label = "Remove Assets" + bl_description = "Remove Selected Assets" @classmethod def poll(cls, context): @@ -51,29 +47,33 @@ class ASSETLIB_OT_remove_assets(Operator): return False sp = context.space_data - if sp.params.asset_library_ref == 'LOCAL': + if sp.params.asset_library_ref == "LOCAL": return False return True def execute(self, context: Context) -> Set[str]: asset = context.active_file - + lib = get_active_library() lib_type = lib.library_type catalog = lib.read_catalog() if not catalog.context.item: - self.report({'ERROR'}, 'The active asset is not in the catalog') - return {'CANCELLED'} + self.report({"ERROR"}, "The active asset is not in the catalog") + return {"CANCELLED"} asset_name = context.asset_file_handle.name - asset_path = lib_type.format_path(asset.asset_data['filepath']) + asset_path = lib_type.format_path(asset.asset_data["filepath"]) asset_catalog = catalog.context.path - - img_path = lib_type.get_image_path(name=asset_name, catalog=asset_catalog, filepath=asset_path) - video_path = lib_type.get_video_path(name=asset_name, catalog=asset_catalog, filepath=asset_path) + + img_path = lib_type.get_image_path( + name=asset_name, catalog=asset_catalog, filepath=asset_path + ) + video_path = lib_type.get_video_path( + name=asset_name, catalog=asset_catalog, filepath=asset_path + ) if asset_path and asset_path.exists(): asset_path.unlink() @@ -81,16 +81,16 @@ class ASSETLIB_OT_remove_assets(Operator): img_path.unlink() if video_path and video_path.exists(): video_path.unlink() - #open_blender_file(filepath) + # open_blender_file(filepath) try: asset_path.parent.rmdir() - except Exception:#Directory not empty + except Exception: # Directory not empty pass bpy.ops.assetlib.bundle(name=lib.name, blocking=True) - return {'FINISHED'} + return {"FINISHED"} class ASSETLIB_OT_edit_data(Operator): @@ -99,12 +99,18 @@ class ASSETLIB_OT_edit_data(Operator): bl_description = "Edit Current Asset Data" bl_options = {"REGISTER", "UNDO"} - warning: StringProperty(name='') - path: StringProperty(name='Path') - catalog: StringProperty(name='Catalog', update=asset_warning_callback, options={'TEXTEDIT_UPDATE'}) - name: StringProperty(name='Name', update=asset_warning_callback, options={'TEXTEDIT_UPDATE'}) - tags: StringProperty(name='Tags', description='Tags need to separate with a comma (,)') - description: StringProperty(name='Description') + warning: StringProperty(name="") + path: StringProperty(name="Path") + catalog: StringProperty( + name="Catalog", update=asset_warning_callback, options={"TEXTEDIT_UPDATE"} + ) + name: StringProperty( + name="Name", update=asset_warning_callback, options={"TEXTEDIT_UPDATE"} + ) + tags: StringProperty( + name="Tags", description="Tags need to separate with a comma (,)" + ) + description: StringProperty(name="Description") @classmethod def poll(cls, context): @@ -120,15 +126,17 @@ class ASSETLIB_OT_edit_data(Operator): lib = prefs.libraries[lib.store_library] new_name = lib.library_type.norm_file_name(self.name) - new_asset_path = lib.library_type.get_asset_path(name=new_name, catalog=self.catalog) + new_asset_path = lib.library_type.get_asset_path( + name=new_name, catalog=self.catalog + ) - #asset_data = lib.library_type.get_asset_data(self.asset) + # asset_data = lib.library_type.get_asset_data(self.asset) asset_data = dict( - tags=[t.strip() for t in self.tags.split(',') if t], + tags=[t.strip() for t in self.tags.split(",") if t], description=self.description, ) - #lib.library_type.set_asset_catalog(asset, asset_data, catalog_data) + # lib.library_type.set_asset_catalog(asset, asset_data, catalog_data) self.asset.name = self.name lib.library_type.set_asset_tags(self.asset, asset_data) lib.library_type.set_asset_info(self.asset, asset_data) @@ -137,37 +145,51 @@ class ASSETLIB_OT_edit_data(Operator): lib.library_type.write_asset(asset=self.asset, asset_path=new_asset_path) if self.old_image_path.exists(): - new_img_path = lib.library_type.get_image_path(new_name, self.catalog, new_asset_path) + new_img_path = lib.library_type.get_image_path( + new_name, self.catalog, new_asset_path + ) self.old_image_path.rename(new_img_path) if self.old_video_path.exists(): - new_video_path = lib.library_type.get_video_path(new_name, self.catalog, new_asset_path) + new_video_path = lib.library_type.get_video_path( + new_name, self.catalog, new_asset_path + ) self.old_video_path.rename(new_video_path) - #if self.old_description_path.exists(): + # if self.old_description_path.exists(): # self.old_description_path.unlink() try: self.old_asset_path.parent.rmdir() - except Exception: #The folder is not empty + except Exception: # The folder is not empty pass - diff_path = Path(bpy.app.tempdir, 'diff.json') - diff = [dict(name=self.old_asset_name, catalog=self.old_catalog, filepath=str(self.old_asset_path), operation='REMOVE')] + diff_path = Path(bpy.app.tempdir, "diff.json") + diff = [ + dict( + name=self.old_asset_name, + catalog=self.old_catalog, + filepath=str(self.old_asset_path), + operation="REMOVE", + ) + ] asset_data = lib.library_type.get_asset_data(self.asset) - diff += [dict(asset_data, - image=str(new_img_path), - filepath=str(new_asset_path), - type=lib.data_type, - library_id=lib.id, - catalog=self.catalog, - operation='ADD' - )] + diff += [ + dict( + asset_data, + image=str(new_img_path), + filepath=str(new_asset_path), + type=lib.data_type, + library_id=lib.id, + catalog=self.catalog, + operation="ADD", + ) + ] print(diff) - diff_path.write_text(json.dumps(diff, indent=4), encoding='utf-8') + diff_path.write_text(json.dumps(diff, indent=4), encoding="utf-8") bpy.ops.assetlib.bundle(name=lib.name, diff=str(diff_path), blocking=True) @@ -182,25 +204,25 @@ class ASSETLIB_OT_edit_data(Operator): lib = get_active_library() if lib.merge_libraries: - layout.prop(lib, 'store_library', expand=False) + layout.prop(lib, "store_library", expand=False) layout.prop(self, "catalog", text="Catalog") layout.prop(self, "name", text="Name") - layout.prop(self, 'tags') - layout.prop(self, 'description') + layout.prop(self, "tags") + layout.prop(self, "description") - #layout.prop() + # layout.prop() layout.separator() col = layout.column() col.use_property_split = False - #row.enabled = False + # row.enabled = False if self.path: col.label(text=self.path) if self.warning: - col.label(icon='ERROR', text=self.warning) + col.label(icon="ERROR", text=self.warning) def invoke(self, context, event): @@ -213,70 +235,74 @@ class ASSETLIB_OT_edit_data(Operator): asset_handle = context.asset_file_handle catalog_file = lib.library_type.read_catalog() - catalog_ids = {v['id']: {'path': k, 'name': v['name']} for k,v in catalog_file.items()} + catalog_ids = { + v["id"]: {"path": k, "name": v["name"]} for k, v in catalog_file.items() + } - #asset_handle = context.asset_file_handle + # asset_handle = context.asset_file_handle self.old_asset_name = asset_handle.name self.old_asset_path = lib.library_type.get_active_asset_path() - - self.asset = load_datablocks(self.old_asset_path, self.old_asset_name, type=lib.data_types) + + self.asset = load_datablocks( + self.old_asset_path, self.old_asset_name, type=lib.data_types + ) if not self.asset: - self.report({'ERROR'}, 'No asset found') + self.report({"ERROR"}, "No asset found") self.name = self.old_asset_name self.description = asset_handle.asset_data.description tags = [t.strip() for t in self.asset.asset_data.tags.keys() if t] - self.tags = ', '.join(tags) - #asset_path - self.old_catalog = catalog_ids[asset_handle.asset_data.catalog_id]['path'] + self.tags = ", ".join(tags) + # asset_path + self.old_catalog = catalog_ids[asset_handle.asset_data.catalog_id]["path"] self.catalog = self.old_catalog - self.old_image_path = lib.library_type.get_image_path(name=self.name, catalog=self.catalog, filepath=self.old_asset_path) - self.old_video_path = lib.library_type.get_video_path(name=self.name, catalog=self.catalog, filepath=self.old_asset_path) - - #self.old_description_path = lib.library_type.get_description_path(self.old_asset_path) - - #self.old_asset_info = lib.library_type.read_asset_info_file(self.old_asset_path) - #self.old_asset_info = lib.library_type.norm_asset_datas([self.old_asset_info])[0] - - + self.old_image_path = lib.library_type.get_image_path( + name=self.name, catalog=self.catalog, filepath=self.old_asset_path + ) + self.old_video_path = lib.library_type.get_video_path( + name=self.name, catalog=self.catalog, filepath=self.old_asset_path + ) + # self.old_description_path = lib.library_type.get_description_path(self.old_asset_path) + # self.old_asset_info = lib.library_type.read_asset_info_file(self.old_asset_path) + # self.old_asset_info = lib.library_type.norm_asset_datas([self.old_asset_info])[0] return context.window_manager.invoke_props_dialog(self, width=450) def cancel(self, context): - print('Cancel Edit Data, removing the asset') + print("Cancel Edit Data, removing the asset") lib = get_active_library() active_lib = lib.library_type.get_active_asset_library() getattr(bpy.data, active_lib.data_types).remove(self.asset) + class ASSETLIB_OT_remove_user_library(Operator): bl_idname = "assetlib.remove_user_library" bl_options = {"REGISTER", "UNDO"} - bl_label = 'Remove User Library' - bl_description = 'Remove User Library' + bl_label = "Remove User Library" + bl_description = "Remove User Library" - index : IntProperty(default=-1) + index: IntProperty(default=-1) def execute(self, context: Context) -> Set[str]: prefs = get_addon_prefs() prefs.user_libraries.remove(self.index) - return {'FINISHED'} + return {"FINISHED"} class ASSETLIB_OT_add_user_library(Operator): bl_idname = "assetlib.add_user_library" bl_options = {"REGISTER", "UNDO"} - bl_label = 'Add User Library' - bl_description = 'Add User Library' - + bl_label = "Add User Library" + bl_description = "Add User Library" def execute(self, context: Context) -> Set[str]: prefs = get_addon_prefs() @@ -284,72 +310,77 @@ class ASSETLIB_OT_add_user_library(Operator): lib = prefs.user_libraries.add() lib.expand = True - return {'FINISHED'} + return {"FINISHED"} class ASSETLIB_OT_open_blend(Operator): bl_idname = "assetlib.open_blend" bl_options = {"REGISTER", "UNDO"} - bl_label = 'Open Blender File' - bl_description = 'Open blender file' + bl_label = "Open Blender File" + bl_description = "Open blender file" - #filepath : StringProperty(subtype='FILE_PATH') + # filepath : StringProperty(subtype='FILE_PATH') def execute(self, context: Context) -> Set[str]: - #asset = context.active_file - #prefs = get_addon_prefs() + # asset = context.active_file + # prefs = get_addon_prefs() lib = get_active_library() - #filepath = lib.library_type.format_path(asset.asset_data['filepath']) + # filepath = lib.library_type.format_path(asset.asset_data['filepath']) filepath = lib.library_type.get_active_asset_path() open_blender_file(filepath) - return {'FINISHED'} + return {"FINISHED"} class ASSETLIB_OT_set_paths(Operator): bl_idname = "assetlib.set_paths" bl_options = {"REGISTER", "UNDO", "INTERNAL"} - bl_label = 'Set Paths' - bl_description = 'Set Library Paths' + bl_label = "Set Paths" + bl_description = "Set Library Paths" name: StringProperty() all: BoolProperty(default=False) def execute(self, context: Context) -> Set[str]: prefs = get_addon_prefs() - print('Set Paths') + print("Set Paths") if self.all: libs = prefs.libraries else: libs = [prefs.libraries[self.name]] - + for lib in libs: lib.clear_library_path() lib.set_library_path() - return {'FINISHED'} + return {"FINISHED"} class ASSETLIB_OT_bundle_library(Operator): bl_idname = "assetlib.bundle" bl_options = {"INTERNAL"} - bl_label = 'Bundle Library' - bl_description = 'Bundle all matching asset found inside one blend' + bl_label = "Bundle Library" + bl_description = "Bundle all matching asset found inside one blend" - name : StringProperty() - diff : StringProperty() - blocking : BoolProperty(default=False) - mode : EnumProperty(items=[(i.replace(' ', '_').upper(), i, '') for i in ('None', 'All', 'Auto Bundle')], default='NONE') - directory : StringProperty(subtype='DIR_PATH') - #conform : BoolProperty(default=False) - #def refresh(self): + name: StringProperty() + diff: StringProperty() + blocking: BoolProperty(default=False) + mode: EnumProperty( + items=[ + (i.replace(" ", "_").upper(), i, "") for i in ("None", "All", "Auto Bundle") + ], + default="NONE", + ) + directory: StringProperty(subtype="DIR_PATH") + # conform : BoolProperty(default=False) + # def refresh(self): # for area in suitable_areas(bpy.context.screen): # bpy.ops.asset.library_refresh({"area": area, 'region': area.regions[3]}) - #space_data.activate_asset_by_id(asset, deferred=deferred) + # space_data.activate_asset_by_id(asset, deferred=deferred) def execute(self, context: Context) -> Set[str]: prefs = get_addon_prefs() @@ -358,19 +389,20 @@ class ASSETLIB_OT_bundle_library(Operator): if self.name: libs += [prefs.libraries[self.name]] - if self.mode == 'ALL': + if self.mode == "ALL": libs += prefs.libraries.values() - elif self.mode == 'AUTO_BUNDLE': + elif self.mode == "AUTO_BUNDLE": libs += [l for l in prefs.libraries if l.auto_bundle] - + if not libs: return {"CANCELLED"} - + lib_datas = [l.to_dict() for l in libs] - print(f'Bundle Libraries: {[l.name for l in libs]}') + print(f"Bundle Libraries: {[l.name for l in libs]}") - script_code = dedent(f""" + script_code = dedent( + f""" import bpy prefs = bpy.context.preferences.addons["asset_library"].preferences @@ -380,52 +412,53 @@ class ASSETLIB_OT_bundle_library(Operator): lib.library_type.bundle(cache_diff='{self.diff}') bpy.ops.wm.quit_blender() - """) + """ + ) - script_path = Path(bpy.app.tempdir) / 'bundle_library.py' + script_path = Path(bpy.app.tempdir) / "bundle_library.py" script_path.write_text(script_code) print(script_code) - #raise Exception() + # raise Exception() cmd = get_bl_cmd(script=str(script_path), background=True) - #print(cmd) + # print(cmd) if self.blocking: subprocess.call(cmd) bpy.app.timers.register(refresh_asset_browsers, first_interval=0.2) else: subprocess.Popen(cmd) - - return {'FINISHED'} + + return {"FINISHED"} class ASSETLIB_OT_reload_addon(Operator): bl_idname = "assetlib.reload_addon" bl_options = {"UNDO"} - bl_label = 'Reload Asset Library Addon' - bl_description = 'Reload The Asset Library Addon and the addapters' + bl_label = "Reload Asset Library Addon" + bl_description = "Reload The Asset Library Addon and the addapters" def execute(self, context: Context) -> Set[str]: - print('Execute reload') + print("Execute reload") asset_library.unregister() importlib.reload(asset_library) asset_library.register() - return {'FINISHED'} + return {"FINISHED"} class ASSETLIB_OT_diff(Operator): bl_idname = "assetlib.diff" bl_options = {"REGISTER", "UNDO"} - bl_label = 'Synchronize' - bl_description = 'Synchronize Action Lib to Local Directory' + bl_label = "Synchronize" + bl_description = "Synchronize Action Lib to Local Directory" - name : StringProperty() - conform : BoolProperty(default=False) + name: StringProperty() + conform: BoolProperty(default=False) def execute(self, context: Context) -> Set[str]: prefs = get_addon_prefs() @@ -433,7 +466,8 @@ class ASSETLIB_OT_diff(Operator): lib = prefs.libraries.get(self.name) lib.library_type.diff() - return {'FINISHED'} + return {"FINISHED"} + ''' class ASSETLIB_OT_conform_library(Operator): @@ -482,24 +516,25 @@ class ASSETLIB_OT_conform_library(Operator): return {'RUNNING_MODAL'} ''' + class ASSETLIB_OT_make_custom_preview(Operator): bl_idname = "assetlib.make_custom_preview" bl_label = "Custom Preview" bl_description = "Set a camera to preview an asset" - image_size : IntProperty(default=512) - modal : BoolProperty(default=False) + image_size: IntProperty(default=512) + modal: BoolProperty(default=False) def modal(self, context, event): - if event.type in {'ESC'}: # Cancel + if event.type in {"ESC"}: # Cancel self.restore() - return {'CANCELLED'} + return {"CANCELLED"} - elif event.type in {'RET', 'NUMPAD_ENTER'}: # Cancel + elif event.type in {"RET", "NUMPAD_ENTER"}: # Cancel return self.execute(context) - #return {'FINISHED'} + # return {'FINISHED'} - return {'PASS_THROUGH'} + return {"PASS_THROUGH"} def execute(self, context): @@ -508,14 +543,13 @@ class ASSETLIB_OT_make_custom_preview(Operator): img_path = context.scene.render.filepath - #print('Load Image to previews') - prefs.previews.load(Path(img_path).stem, img_path, 'IMAGE') - #img = bpy.data.images.load(context.scene.render.filepath) - #img.update() - #img.preview_ensure() + # print('Load Image to previews') + prefs.previews.load(Path(img_path).stem, img_path, "IMAGE") + # img = bpy.data.images.load(context.scene.render.filepath) + # img.update() + # img.preview_ensure() - - #Copy the image with a new name + # Copy the image with a new name # render = bpy.data.images['Render Result'] # render_pixels = [0] * self.image_size * self.image_size * 4 @@ -523,32 +557,30 @@ class ASSETLIB_OT_make_custom_preview(Operator): # img = bpy.data.images.new(name=img_name, width=self.image_size, height=self.image_size, is_data=True, alpha=True) # img.pixels.foreach_set(render_pixels) - #img.scale(128, 128) - #img.preview_ensure() + # img.scale(128, 128) + # img.preview_ensure() # preview_size = render.size # pixels = [0] * preview_size[0] * preview_size[1] * 4 # render.pixels.foreach_get(pixels) - + # image.preview.image_size = preview_size # image.preview.image_pixels_float.foreach_set(pixels) - - self.restore() - #self.is_running = False + # self.is_running = False prefs.preview_modal = False return {"FINISHED"} def restore(self): - print('RESTORE') + print("RESTORE") try: - bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW') + bpy.types.SpaceView3D.draw_handler_remove(self._handle, "WINDOW") except: - print('Failed remove handler') + print("Failed remove handler") pass bpy.data.objects.remove(self.camera) @@ -562,27 +594,27 @@ class ASSETLIB_OT_make_custom_preview(Operator): bg_color = (0.8, 0.1, 0.1, 0.5) font_color = (1, 1, 1, 1) - text = f'Escape: Cancel Enter: Make Preview' + text = f"Escape: Cancel Enter: Make Preview" font_id = 0 dim = blf.dimensions(font_id, text) - #gpu.state.line_width_set(100) + # gpu.state.line_width_set(100) # bgl.glLineWidth(100) # self.shader_2d.bind() # self.shader_2d.uniform_float("color", bg_color) # self.screen_framing.draw(self.shader_2d) - + # # Reset # gpu.state.line_width_set(1) # -dim[0]/2, +dim[1]/2 + 5 # Display Text - blf.color(font_id, *font_color) # unpack color - blf.position(font_id, context.region.width/2 -dim[0]/2, dim[1]/2 + 5, 0) + blf.color(font_id, *font_color) # unpack color + blf.position(font_id, context.region.width / 2 - dim[0] / 2, dim[1] / 2 + 5, 0) blf.size(font_id, 12, dpi) - blf.draw(font_id, f'Escape: Cancel Enter: Make Preview') - + blf.draw(font_id, f"Escape: Cancel Enter: Make Preview") + def get_image_name(self): prefs = get_addon_prefs() preview_names = [p for p in prefs.previews.keys()] @@ -592,45 +624,47 @@ class ASSETLIB_OT_make_custom_preview(Operator): if preview_names: index = int(preview_names[-1][-2:]) + 1 - return f'preview_{index:03d}' + return f"preview_{index:03d}" def invoke(self, context, event): prefs = get_addon_prefs() - cam_data = bpy.data.cameras.new(name='Preview Camera') - self.camera = bpy.data.objects.new(name='Preview Camera', object_data=cam_data) + cam_data = bpy.data.cameras.new(name="Preview Camera") + self.camera = bpy.data.objects.new(name="Preview Camera", object_data=cam_data) - #view_3d = get_view3d_persp() + # view_3d = get_view3d_persp() scn = context.scene space = context.space_data matrix = space.region_3d.view_matrix.inverted() - if space.region_3d.view_perspective == 'CAMERA': + if space.region_3d.view_perspective == "CAMERA": matrix = scn.camera.matrix_world - + self.camera.matrix_world = matrix img_name = self.get_image_name() - img_path = Path(bpy.app.tempdir, img_name).with_suffix('.webp') + img_path = Path(bpy.app.tempdir, img_name).with_suffix(".webp") - self.attr_changed = attr_set([ - (space.overlay, 'show_overlays', False), - (space.region_3d, 'view_perspective', 'CAMERA'), - (space.region_3d, 'view_camera_offset'), - (space.region_3d, 'view_camera_zoom'), - (space, 'lock_camera', True), - (space, 'show_region_ui', False), - (scn, 'camera', self.camera), - (scn.render, 'resolution_percentage', 100), - (scn.render, 'resolution_x', self.image_size), - (scn.render, 'resolution_y', self.image_size), - (scn.render, 'film_transparent', True), - (scn.render.image_settings, 'file_format', 'WEBP'), - (scn.render.image_settings, 'color_mode', 'RGBA'), - #(scn.render.image_settings, 'color_depth', '8'), - (scn.render, 'use_overwrite', True), - (scn.render, 'filepath', str(img_path)), - ]) + self.attr_changed = attr_set( + [ + (space.overlay, "show_overlays", False), + (space.region_3d, "view_perspective", "CAMERA"), + (space.region_3d, "view_camera_offset"), + (space.region_3d, "view_camera_zoom"), + (space, "lock_camera", True), + (space, "show_region_ui", False), + (scn, "camera", self.camera), + (scn.render, "resolution_percentage", 100), + (scn.render, "resolution_x", self.image_size), + (scn.render, "resolution_y", self.image_size), + (scn.render, "film_transparent", True), + (scn.render.image_settings, "file_format", "WEBP"), + (scn.render.image_settings, "color_mode", "RGBA"), + # (scn.render.image_settings, 'color_depth', '8'), + (scn.render, "use_overwrite", True), + (scn.render, "filepath", str(img_path)), + ] + ) bpy.ops.view3d.view_center_camera() space.region_3d.view_camera_zoom -= 6 @@ -643,14 +677,17 @@ class ASSETLIB_OT_make_custom_preview(Operator): if self.modal: prefs.preview_modal = True - self.shader_2d = gpu.shader.from_builtin('2D_UNIFORM_COLOR') + self.shader_2d = gpu.shader.from_builtin("2D_UNIFORM_COLOR") self.screen_framing = batch_for_shader( - self.shader_2d, 'LINE_LOOP', {"pos": [(0,0), (0,h), (w,h), (w,0)]}) + self.shader_2d, "LINE_LOOP", {"pos": [(0, 0), (0, h), (w, h), (w, 0)]} + ) - self._handle = bpy.types.SpaceView3D.draw_handler_add(self.draw_callback_px, (context,), 'WINDOW', 'POST_PIXEL') + self._handle = bpy.types.SpaceView3D.draw_handler_add( + self.draw_callback_px, (context,), "WINDOW", "POST_PIXEL" + ) context.window_manager.modal_handler_add(self) - return {'RUNNING_MODAL'} + return {"RUNNING_MODAL"} else: return self.execute(context) @@ -661,10 +698,10 @@ class ASSETLIB_OT_generate_previews(Operator): bl_label = "Generate Previews" bl_description = "Generate and write the image for assets" - cache : StringProperty() - preview_blend : StringProperty() - name : StringProperty() - blocking : BoolProperty(default=True) + cache: StringProperty() + preview_blend: StringProperty() + name: StringProperty() + blocking: BoolProperty(default=True) def execute(self, context: Context) -> Set[str]: prefs = get_addon_prefs() @@ -678,17 +715,18 @@ class ASSETLIB_OT_generate_previews(Operator): # cmd = [ # bpy.app.binary_path, '-b', '--use-system-env', # '--python', str(PREVIEW_ASSETS_SCRIPT), '--', - # '--preview-blend', str(self.preview_blend), + # '--preview-blend', str(self.preview_blend), # '--preview-assets-file', str(self.preview_assets_file) # ] # subprocess.call(cmd) preview_blend = self.preview_blend or lib.library_type.preview_blend - - if not preview_blend or not Path(preview_blend).exists(): - preview_blend = MODULE_DIR / 'common' / 'preview.blend' - script_path = Path(bpy.app.tempdir) / 'generate_previews.py' - script_code = dedent(f""" + if not preview_blend or not Path(preview_blend).exists(): + preview_blend = MODULE_DIR / "common" / "preview.blend" + + script_path = Path(bpy.app.tempdir) / "generate_previews.py" + script_code = dedent( + f""" import bpy prefs = bpy.context.preferences.addons["asset_library"].preferences lib = prefs.env_libraries.add() @@ -696,7 +734,8 @@ class ASSETLIB_OT_generate_previews(Operator): bpy.ops.wm.open_mainfile(filepath='{preview_blend}', load_ui=True) lib.library_type.generate_previews(cache='{self.cache}') - """) + """ + ) script_path.write_text(script_code) @@ -707,15 +746,14 @@ class ASSETLIB_OT_generate_previews(Operator): else: subprocess.Popen(cmd) - - return {'FINISHED'} + return {"FINISHED"} class ASSETLIB_OT_play_preview(Operator): bl_idname = "assetlib.play_preview" bl_options = {"REGISTER", "UNDO", "INTERNAL"} - bl_label = 'Play Preview' - bl_description = 'Play Preview' + bl_label = "Play Preview" + bl_description = "Play Preview" @classmethod def poll(cls, context: Context) -> bool: @@ -738,64 +776,63 @@ class ASSETLIB_OT_play_preview(Operator): lib = get_active_library() - #filepath = lib.library_type.format_path(asset.asset_data['filepath']) + # filepath = lib.library_type.format_path(asset.asset_data['filepath']) asset_path = lib.library_type.get_active_asset_path() asset_image = lib.library_type.get_image(asset.name, asset_path) asset_video = lib.library_type.get_video(asset.name, asset_path) - + if not asset_image and not asset_video: - self.report({'ERROR'}, f'Preview for {asset.name} not found.') + self.report({"ERROR"}, f"Preview for {asset.name} not found.") return {"CANCELLED"} - + if asset_video: - self.report({'INFO'}, f'Video found. {asset_video}.') + self.report({"INFO"}, f"Video found. {asset_video}.") if prefs.video_player: subprocess.Popen([prefs.video_player, asset_video]) else: bpy.ops.wm.path_open(filepath=str(asset_video)) else: - self.report({'INFO'}, f'Image found. {asset_image}.') + self.report({"INFO"}, f"Image found. {asset_image}.") if prefs.image_player: subprocess.Popen([prefs.image_player, asset_image]) else: bpy.ops.wm.path_open(filepath=str(asset_image)) - return {"FINISHED"} class ASSETLIB_OT_synchronize(Operator): bl_idname = "assetlib.synchronize" bl_options = {"REGISTER", "UNDO"} - bl_label = 'Synchronize' - bl_description = 'Synchronize Action Lib to Local Directory' + bl_label = "Synchronize" + bl_description = "Synchronize Action Lib to Local Directory" - clean : BoolProperty(default=False) - only_new : BoolProperty(default=False) - only_recent : BoolProperty(default=False) + clean: BoolProperty(default=False) + only_new: BoolProperty(default=False) + only_recent: BoolProperty(default=False) name: StringProperty() all: BoolProperty(default=False) def execute(self, context: Context) -> Set[str]: - print('Not yet Implemented, have to be replace by Bundle instead') - return {'FINISHED'} + print("Not yet Implemented, have to be replace by Bundle instead") + return {"FINISHED"} prefs = get_addon_prefs() - print('Synchronize') + print("Synchronize") if self.all: libs = prefs.libraries else: libs = [prefs.libraries.get(self.name)] - for lib in libs: + for lib in libs: if self.clean and Path(lib.path_local).exists(): pass - print('To check first') - #shutil.rmtree(path_local) + print("To check first") + # shutil.rmtree(path_local) if not lib.path_local: continue @@ -804,10 +841,11 @@ class ASSETLIB_OT_synchronize(Operator): src=lib.path, dst=lib.path_local, only_new=self.only_new, - only_recent=self.only_recent + only_recent=self.only_recent, ) - return {'FINISHED'} + return {"FINISHED"} + classes = ( ASSETLIB_OT_play_preview, @@ -821,17 +859,19 @@ classes = ( ASSETLIB_OT_bundle_library, ASSETLIB_OT_remove_assets, ASSETLIB_OT_edit_data, - #ASSETLIB_OT_conform_library, + # ASSETLIB_OT_conform_library, ASSETLIB_OT_reload_addon, - ASSETLIB_OT_make_custom_preview + ASSETLIB_OT_make_custom_preview, ) + def register(): - #bpy.types.UserAssetLibrary.is_env = False + # bpy.types.UserAssetLibrary.is_env = False for cls in classes: bpy.utils.register_class(cls) + def unregister(): for cls in reversed(classes): - bpy.utils.unregister_class(cls) \ No newline at end of file + bpy.utils.unregister_class(cls) diff --git a/pose/__init__.py b/pose/__init__.py index 262497a..4d90b68 100644 --- a/pose/__init__.py +++ b/pose/__init__.py @@ -1,14 +1,14 @@ +from asset_library.pose import operators -from asset_library.pose import ( - operators) - -if 'bpy' in locals(): +if "bpy" in locals(): import importlib importlib.reload(operators) + def register(): operators.register() + def unregister(): - operators.unregister() \ No newline at end of file + operators.unregister() diff --git a/pose/conversion.py b/pose/conversion.py index d61bc89..4adeebc 100644 --- a/pose/conversion.py +++ b/pose/conversion.py @@ -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 # colour), but the old-style poselib doesn't contain such information. All # 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 diff --git a/pose/operators.py b/pose/operators.py index b5dbfd4..70c5f32 100644 --- a/pose/operators.py +++ b/pose/operators.py @@ -22,7 +22,13 @@ import subprocess import uuid import time -from bpy.props import BoolProperty, CollectionProperty, EnumProperty, PointerProperty, StringProperty +from bpy.props import ( + BoolProperty, + CollectionProperty, + EnumProperty, + PointerProperty, + StringProperty, +) from bpy.types import ( Action, Context, @@ -40,11 +46,7 @@ from asset_library.action.functions import ( 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): @@ -59,25 +61,28 @@ class POSELIB_OT_create_pose_asset(Operator): pose_name: StringProperty(name="Pose Name") # type: ignore activate_new_action: BoolProperty(name="Activate New Action", default=True) # type: ignore - @classmethod 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. - asset_browse_area: Optional[bpy.types.Area] = asset_browser.area_from_context(context) + asset_browse_area: Optional[bpy.types.Area] = asset_browser.area_from_context( + context + ) if not asset_browse_area: # No asset browser is visible, so there also aren't any expectations # that this asset will be visible. return True - + asset_space_params = asset_browser.params(asset_browse_area) - if asset_space_params.asset_library_ref != 'LOCAL': - cls.poll_message_set("Asset Browser must be set to the Current File library") + if asset_space_params.asset_library_ref != "LOCAL": + cls.poll_message_set( + "Asset Browser must be set to the Current File library" + ) return False - + return True def execute(self, context: Context) -> Set[str]: - #pose_name = self.pose_name or context.object.name + # pose_name = self.pose_name or context.object.name pose_name = False if context.object.animation_data: if context.object.animation_data.action: @@ -85,28 +90,28 @@ class POSELIB_OT_create_pose_asset(Operator): if pose_name: 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: - 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: - 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(): - pose_name = f'hand_{pose_name}' - - if pose_name.startswith('lips_'): - pose_name.replace('lips_', '') - split = pose_name.split('_') - pose_name = '-'.join([s for s in split if s.isupper()]) - pose_name = f'{pose_name}_{split[-1]}' + if "hands" in context.object.animation_data.action.name.lower(): + pose_name = f"hand_{pose_name}" + + if pose_name.startswith("lips_"): + pose_name.replace("lips_", "") + split = pose_name.split("_") + pose_name = "-".join([s for s in split if s.isupper()]) + pose_name = f"{pose_name}_{split[-1]}" prefix = False - + if prefix and not pose_name.startswith(asset_name): - pose_name = f'{asset_name}_{pose_name}' + pose_name = f"{asset_name}_{pose_name}" else: pose_name = self.pose_name or context.object.name @@ -126,7 +131,6 @@ class POSELIB_OT_create_pose_asset(Operator): if context.scene.camera: data_dict.update(dict(camera=context.scene.camera.name)) - for k, v in data_dict.items(): data[k] = v ### @@ -134,7 +138,7 @@ class POSELIB_OT_create_pose_asset(Operator): if self.activate_new_action: self._set_active_action(context, asset) self._activate_asset_in_browser(context, asset) - return {'FINISHED'} + return {"FINISHED"} def _set_active_action(self, context: Context, asset: Action) -> None: self._prevent_action_loss(context.object) @@ -149,7 +153,9 @@ class POSELIB_OT_create_pose_asset(Operator): This makes it possible to immediately check & edit the created pose asset. """ - asset_browse_area: Optional[bpy.types.Area] = asset_browser.area_from_context(context) + asset_browse_area: Optional[bpy.types.Area] = asset_browser.area_from_context( + context + ) if not asset_browse_area: return @@ -181,7 +187,9 @@ class POSELIB_OT_create_pose_asset(Operator): return action.use_fake_user = True - self.report({'WARNING'}, "Action %s marked Fake User to prevent loss" % action.name) + self.report( + {"WARNING"}, "Action %s marked Fake User to prevent loss" % action.name + ) class POSELIB_OT_restore_previous_action(Operator): @@ -215,17 +223,17 @@ class POSELIB_OT_restore_previous_action(Operator): self._timer = wm.event_timer_add(0.001, window=context.window) wm.modal_handler_add(self) - return {'RUNNING_MODAL'} + return {"RUNNING_MODAL"} def modal(self, context, event): - if event.type != 'TIMER': - return {'RUNNING_MODAL'} + if event.type != "TIMER": + return {"RUNNING_MODAL"} wm = context.window_manager wm.event_timer_remove(self._timer) context.object.pose.apply_pose_from_action(self.pose_action) - return {'FINISHED'} + return {"FINISHED"} class ASSET_OT_assign_action(Operator): @@ -257,7 +265,9 @@ class ASSET_OT_assign_action(Operator): class POSELIB_OT_copy_as_asset(Operator): bl_idname = "poselib.copy_as_asset" bl_label = "Copy Pose As Asset" - bl_description = "Create a new pose asset on the clipboard, to be pasted into an Asset Browser" + bl_description = ( + "Create a new pose asset on the clipboard, to be pasted into an Asset Browser" + ) bl_options = {"REGISTER"} CLIPBOARD_ASSET_MARKER = "ASSET-BLEND=" @@ -289,7 +299,10 @@ class POSELIB_OT_copy_as_asset(Operator): filepath, ) asset_browser.tag_redraw(context.screen) - self.report({"INFO"}, "Pose Asset copied, use Paste As New Asset in any Asset Browser to paste") + self.report( + {"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. asset.asset_clear() @@ -300,7 +313,10 @@ class POSELIB_OT_copy_as_asset(Operator): if asset.users > 0: # 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. - self.report({"WARNING"}, "Unexpected non-zero user count for the asset, please report this as a bug") + self.report( + {"WARNING"}, + "Unexpected non-zero user count for the asset, please report this as a bug", + ) bpy.data.actions.remove(asset) return {"FINISHED"} @@ -331,8 +347,10 @@ class POSELIB_OT_paste_asset(Operator): return False asset_lib_ref = context.space_data.params.asset_library_ref - if asset_lib_ref != 'LOCAL': - cls.poll_message_set("Asset Browser must be set to the Current File library") + if asset_lib_ref != "LOCAL": + cls.poll_message_set( + "Asset Browser must be set to the Current File library" + ) return False # Delay checking the clipboard as much as possible, as it's CPU-heavier than the other checks. @@ -348,7 +366,6 @@ class POSELIB_OT_paste_asset(Operator): return True - def execute(self, context: Context) -> Set[str]: clipboard = context.window_manager.clipboard marker_len = len(POSELIB_OT_copy_as_asset.CLIPBOARD_ASSET_MARKER) @@ -379,18 +396,18 @@ class POSELIB_OT_paste_asset(Operator): class POSELIB_OT_pose_asset_select_bones(Operator): bl_idname = "poselib.pose_asset_select_bones" bl_label = "Select Bones" - #bl_description = "Select those bones that are used in this pose" + # bl_description = "Select those bones that are used in this pose" bl_description = "Click: Select used Bones\nAlt+Click: Select Flipped Bones\nCtrl+Click: Select Both sides." bl_options = {"REGISTER", "UNDO"} - #bl_property = "selected_side" + # bl_property = "selected_side" selected_side: EnumProperty( - name='Selected Side', + name="Selected Side", items=( - ('CURRENT', "Current", ""), - ('FLIPPED', "Flipped", ""), - ('BOTH', "Both", ""), - ) + ("CURRENT", "Current", ""), + ("FLIPPED", "Flipped", ""), + ("BOTH", "Both", ""), + ), ) @classmethod @@ -402,7 +419,7 @@ class POSELIB_OT_pose_asset_select_bones(Operator): and context.asset_file_handle ): 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]: asset: FileSelectEntry = context.asset_file_handle @@ -417,7 +434,9 @@ class POSELIB_OT_pose_asset_select_bones(Operator): def _load_and_use_pose(self, context: Context) -> Set[str]: asset_library_ref = context.asset_library_ref asset = context.asset_file_handle - asset_lib_path = bpy.types.AssetHandle.get_full_library_path(asset, asset_library_ref) + asset_lib_path = bpy.types.AssetHandle.get_full_library_path( + asset, asset_library_ref + ) if not asset_lib_path: self.report( # type: ignore @@ -426,7 +445,7 @@ class POSELIB_OT_pose_asset_select_bones(Operator): f"Selected asset {asset.name} could not be located inside the asset library", ) return {"CANCELLED"} - if asset.id_type != 'ACTION': + if asset.id_type != "ACTION": self.report( # type: ignore {"ERROR"}, f"Selected asset {asset.name} is not an Action", @@ -442,10 +461,13 @@ class POSELIB_OT_pose_asset_select_bones(Operator): def use_pose(self, context: Context, pose_asset: Action) -> Set[str]: 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, selected_side=self.selected_side) + # pose_usage.select_bones(arm_object, pose_asset, select=self.select, flipped=self.flipped) + pose_usage.select_bones( + arm_object, pose_asset, selected_side=self.selected_side + ) return {"FINISHED"} + # This operator takes the Window Manager's `actionlib_flipped` property, and # 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 @@ -464,7 +486,7 @@ class POSELIB_OT_blend_pose_asset_for_keymap(Operator): @classmethod def poll(cls, context: Context) -> bool: return bpy.ops.poselib.blend_pose_asset.poll(context.copy()) - + """ def invoke(self, context, event): if event.type == 'LEFTMOUSE': @@ -478,10 +500,14 @@ class POSELIB_OT_blend_pose_asset_for_keymap(Operator): """ def invoke(self, context: Context, event: Event) -> Set[str]: - return bpy.ops.poselib.blend_pose_asset(context.copy(), 'INVOKE_DEFAULT', flipped=self.flipped) + return bpy.ops.poselib.blend_pose_asset( + context.copy(), "INVOKE_DEFAULT", flipped=self.flipped + ) def execute(self, context: Context) -> Set[str]: - return bpy.ops.poselib.blend_pose_asset(context.copy(), 'EXEC_DEFAULT', flipped=self.flipped) + return bpy.ops.poselib.blend_pose_asset( + context.copy(), "EXEC_DEFAULT", flipped=self.flipped + ) # This operator takes the Window Manager's `actionlib_flipped` property, and @@ -489,14 +515,15 @@ class POSELIB_OT_blend_pose_asset_for_keymap(Operator): # possible to bind a key to the operator and still have it respect the global # "Flip Pose" checkbox. + class POSELIB_OT_apply_pose_asset_for_keymap(Operator): bl_idname = "poselib.apply_pose_asset_for_keymap" bl_options = {"REGISTER", "UNDO", "INTERNAL"} _rna = bpy.ops.poselib.apply_pose_asset.get_rna_type() bl_label = _rna.name - #bl_description = _rna.description - bl_description = 'Apply Pose to Bones' + # bl_description = _rna.description + bl_description = "Apply Pose to Bones" del _rna flipped: BoolProperty(name="Flipped", default=False) # type: ignore @@ -506,27 +533,42 @@ class POSELIB_OT_apply_pose_asset_for_keymap(Operator): if not asset_utils.SpaceAssetInfo.is_asset_browser(context.space_data): return False return bpy.ops.poselib.apply_pose_asset.poll(context.copy()) - + def execute(self, context: Context) -> Set[str]: if self.flipped: action = bpy.data.actions.get(context.active_file.name) store_bones = {} - + bones = [ - 'blendshape-eyes', 'blendshape-eye.L', 'blendshape-eye.R', - 'blendshape-corner-mouth', 'blendshape-corner-mouth.L', - 'blendshape-corner-down-mouth.L', 'blendshape-corner-up-mouth.L', - 'blendshape-corner-mouth-add.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', + "blendshape-eyes", + "blendshape-eye.L", + "blendshape-eye.R", + "blendshape-corner-mouth", + "blendshape-corner-mouth.L", + "blendshape-corner-down-mouth.L", + "blendshape-corner-up-mouth.L", + "blendshape-corner-mouth-add.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 = [ - 'location', 'rotation_quaternion', - 'rotation_euler', 'rotation_axis_angle', 'scale' + "location", + "rotation_quaternion", + "rotation_euler", + "rotation_axis_angle", + "scale", ] if action: @@ -534,7 +576,7 @@ class POSELIB_OT_apply_pose_asset_for_keymap(Operator): bone_name, prop_name = split_path(fc.data_path) if bone_name not in bones: continue - + if not bone_name in store_bones.keys(): store_bones[bone_name] = {} @@ -543,30 +585,40 @@ class POSELIB_OT_apply_pose_asset_for_keymap(Operator): if not prop_name in store_bones[bone_name].keys(): store_bones[bone_name][prop_name] = [] - val = getattr(context.object.pose.bones[bone_name], prop_name) + val = getattr( + context.object.pose.bones[bone_name], prop_name + ) - store_bones[bone_name][prop_name].append(fc.evaluate(context.scene.frame_current)) - - bpy.ops.poselib.apply_pose_asset(context.copy(), 'EXEC_DEFAULT', flipped=True) + store_bones[bone_name][prop_name].append( + fc.evaluate(context.scene.frame_current) + ) + + bpy.ops.poselib.apply_pose_asset( + context.copy(), "EXEC_DEFAULT", flipped=True + ) for bone, v in store_bones.items(): for attr, attr_val in v.items(): flipped_vector = 1 - + ### 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) - if attr == 'location': + if attr == "location": flipped_vector = Vector((-1, 1, 1)) # print('-----', store_bones.get(flipped_bone)[attr]) - attr_val = Vector(store_bones.get(flipped_bone)[attr]) * flipped_vector - + attr_val = ( + Vector(store_bones.get(flipped_bone)[attr]) * flipped_vector + ) + setattr(context.object.pose.bones[bone], attr, attr_val) - - return {'FINISHED'} + + return {"FINISHED"} else: - return bpy.ops.poselib.apply_pose_asset(context.copy(), 'EXEC_DEFAULT', flipped=False) + return bpy.ops.poselib.apply_pose_asset( + context.copy(), "EXEC_DEFAULT", flipped=False + ) class POSELIB_OT_convert_old_poselib(Operator): @@ -577,12 +629,18 @@ class POSELIB_OT_convert_old_poselib(Operator): @classmethod def poll(cls, context: Context) -> bool: - action = context.object and context.object.animation_data and context.object.animation_data.action + action = ( + context.object + and context.object.animation_data + and context.object.animation_data.action + ) if not action: cls.poll_message_set("Active object has no Action") return False if not action.pose_markers: - cls.poll_message_set("Action %r is not a old-style pose library" % action.name) + cls.poll_message_set( + "Action %r is not a old-style pose library" % action.name + ) return False return True @@ -593,12 +651,11 @@ class POSELIB_OT_convert_old_poselib(Operator): new_actions = conversion.convert_old_poselib(old_poselib) if not new_actions: - self.report({'ERROR'}, "Unable to convert to pose assets") - return {'CANCELLED'} - - self.report({'INFO'}, "Converted %d poses to pose assets" % len(new_actions)) - return {'FINISHED'} + self.report({"ERROR"}, "Unable to convert to pose assets") + return {"CANCELLED"} + self.report({"INFO"}, "Converted %d poses to pose assets" % len(new_actions)) + return {"FINISHED"} classes = ( @@ -609,7 +666,7 @@ classes = ( POSELIB_OT_create_pose_asset, POSELIB_OT_paste_asset, POSELIB_OT_pose_asset_select_bones, - POSELIB_OT_restore_previous_action + POSELIB_OT_restore_previous_action, ) register, unregister = bpy.utils.register_classes_factory(classes) diff --git a/pose/pose_creation.py b/pose/pose_creation.py index 02a9c90..86c9db9 100644 --- a/pose/pose_creation.py +++ b/pose/pose_creation.py @@ -129,7 +129,9 @@ class PoseActionCreator: continue try: - value = self._current_value(armature_ob, fcurve.data_path, fcurve.array_index) + value = self._current_value( + armature_ob, fcurve.data_path, fcurve.array_index + ) except UnresolvablePathError: # A once-animated property no longer exists. continue @@ -197,7 +199,9 @@ class PoseActionCreator: fcurve: Optional[FCurve] = dst_action.fcurves.find(rna_path, index=array_index) if fcurve is None: - fcurve = dst_action.fcurves.new(rna_path, index=array_index, action_group=bone_name) + fcurve = dst_action.fcurves.new( + rna_path, index=array_index, action_group=bone_name + ) fcurve.keyframe_points.insert(self.params.src_frame_nr, value=value) fcurve.update() @@ -296,10 +300,13 @@ def create_pose_asset( pose_action.asset_generate_preview() 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) -> Optional[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 +) -> Optional[Action]: """Create Action asset from active object & selected bones.""" - + bones = context.selected_pose_bones_from_active_object bone_names = {bone.name for bone in bones} @@ -369,7 +376,10 @@ def copy_keyframe(dst_fcurve: FCurve, src_keyframe: Keyframe) -> Keyframe: """Copy a keyframe from one FCurve to the other.""" dst_keyframe = dst_fcurve.keyframe_points.insert( - src_keyframe.co.x, src_keyframe.co.y, options={'FAST'}, keyframe_type=src_keyframe.type + src_keyframe.co.x, + src_keyframe.co.y, + options={"FAST"}, + keyframe_type=src_keyframe.type, ) for propname in { @@ -412,7 +422,9 @@ def find_keyframe(fcurve: FCurve, frame: float) -> Optional[Keyframe]: return None -def assign_from_asset_browser(asset: Action, asset_browser_area: bpy.types.Area) -> None: +def assign_from_asset_browser( + asset: Action, asset_browser_area: bpy.types.Area +) -> None: """Assign some things from the asset browser to the asset. This sets the current catalog ID, and in the future could include tags diff --git a/pose/pose_usage.py b/pose/pose_usage.py index b492fdf..1d5b4da 100644 --- a/pose/pose_usage.py +++ b/pose/pose_usage.py @@ -14,7 +14,7 @@ from bpy.types import ( ) -#def select_bones(arm_object: Object, action: Action, *, select: bool, flipped: bool, both=False) -> None: +# def select_bones(arm_object: Object, action: Action, *, select: bool, flipped: bool, both=False) -> None: def select_bones(arm_object: Object, action: Action, *, selected_side, toggle=True): pose_bone_re = re.compile(r'pose.bones\["([^"]+)"\]') pose = arm_object.pose @@ -34,15 +34,14 @@ def select_bones(arm_object: Object, action: Action, *, selected_side, toggle=Tr if bone_name in seen_bone_names: continue seen_bone_names.add(bone_name) - - if selected_side == 'FLIPPED': + + if selected_side == "FLIPPED": 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) - elif selected_side == 'CURRENT': + elif selected_side == "CURRENT": bones_to_select.add(bone_name) - for bone in bones_to_select: pose_bone = pose.bones.get(bone) @@ -174,7 +173,7 @@ def flip_side_name(to_flip: str) -> str: return prefix + replace + suffix + number -if __name__ == '__main__': +if __name__ == "__main__": import doctest print(f"Test result: {doctest.testmod()}") diff --git a/preferences.py b/preferences.py index 98dc4c1..9570ddb 100644 --- a/preferences.py +++ b/preferences.py @@ -1,20 +1,32 @@ - import bpy import os from os.path import abspath, join -from bpy.types import (AddonPreferences, PointerProperty, PropertyGroup) -from bpy.props import (BoolProperty, StringProperty, CollectionProperty, - EnumProperty, IntProperty) - -from asset_library.constants import (DATA_TYPES, DATA_TYPE_ITEMS, - ICONS, RESOURCES_DIR, LIBRARY_TYPE_DIR, LIBRARY_TYPES, ADAPTERS) +from bpy.types import AddonPreferences, PointerProperty, PropertyGroup +from bpy.props import ( + BoolProperty, + StringProperty, + CollectionProperty, + EnumProperty, + IntProperty, +) + +from asset_library.constants import ( + DATA_TYPES, + DATA_TYPE_ITEMS, + ICONS, + RESOURCES_DIR, + LIBRARY_TYPE_DIR, + LIBRARY_TYPES, + ADAPTERS, +) from asset_library.common.file_utils import import_module_from_path, norm_str from asset_library.common.bl_utils import get_addon_prefs from asset_library.common.library_cache import LibraryCache from asset_library.common.catalog import Catalog -#from asset_library.common.functions import get_catalog_path + +# from asset_library.common.functions import get_catalog_path from pathlib import Path import importlib @@ -22,120 +34,142 @@ import inspect def update_library_config(self, context): - print('update_library_config not yet implemented') + print("update_library_config not yet implemented") + def update_library_path(self, context): prefs = get_addon_prefs() - self['bundle_directory'] = str(self.library_path) + self["bundle_directory"] = str(self.library_path) if not self.custom_bundle_name: - self['custom_bundle_name'] = self.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) + self["custom_bundle_directory"] = str(custom_bundle_dir) - #if self.custom_bundle_directory: + # if self.custom_bundle_directory: # self['custom_bundle_directory'] = abspath(bpy.path.abspath(self.custom_bundle_directory)) - #else: + # 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') + # print('update_all_assetlib_paths') prefs = get_addon_prefs() - #if self.custom_bundle_directory: + # 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() + # lib.set_library_path() + def get_library_type_items(self, context): - #prefs = get_addon_prefs() + # 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)] + 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)] +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] + 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() + # prefs = get_addon_prefs() - #libraries = [l for l in prefs.libraries if l.merge_library == self.name] + # 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)] + 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')) + 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')) + 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') + 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") - bundle_directory : StringProperty( - name="Bundle Directory", - subtype='DIR_PATH', - default='' + # 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( + 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 + subtype="DIR_PATH", + default="", + update=update_library_path, ) - #use_merge : BoolProperty(default=False, 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) + 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 + # 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) + expand_extra: BoolProperty(name="Expand", default=False) + blend_depth: IntProperty(name="Blend Depth", default=1) # source_directory : StringProperty( # name="Path", @@ -144,15 +178,14 @@ class AssetLibrary(PropertyGroup): # 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) - #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) - adapters : bpy.props.PointerProperty(type=Adapters) - adapter_name : EnumProperty(items=get_adapters_items) - - parent_name : StringProperty() + parent_name: StringProperty() # data_file_path : StringProperty( # name="Path", @@ -160,7 +193,7 @@ class AssetLibrary(PropertyGroup): # default='', # ) - #def __init__(self): + # def __init__(self): # self.library_types.parent = self @property @@ -171,20 +204,24 @@ class AssetLibrary(PropertyGroup): @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)] + 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)] + 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' + if data_type == "FILE": + data_type = "COLLECTION" + return f"{data_type.lower()}s" @property def library_type(self): @@ -207,10 +244,10 @@ class AssetLibrary(PropertyGroup): prefs = get_addon_prefs() asset_lib_ref = bpy.context.space_data.params.asset_library_ref - #TODO work also outside asset_library_area + # TODO work also outside asset_library_area if asset_lib_ref not in prefs.libraries: return None - + return prefs.libraries[asset_lib_ref] @property @@ -218,7 +255,7 @@ class AssetLibrary(PropertyGroup): prefs = get_addon_prefs() library_name = self.library_name - #if not self.use_custom_bundle_name: + # if not self.use_custom_bundle_name: # library_name = norm_str(library_name) if self.use_custom_bundle_directory: @@ -235,9 +272,9 @@ class AssetLibrary(PropertyGroup): 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() @@ -248,32 +285,32 @@ class AssetLibrary(PropertyGroup): return LibraryCache.from_library(self).read() def clear_library_path(self): - #print('Clear Library Path', self.name) + # print('Clear Library Path', self.name) prefs = bpy.context.preferences libs = prefs.filepaths.asset_libraries - #path = self.library_path.as_posix() - + # path = self.library_path.as_posix() + for l in reversed(libs): - #lib_path = Path(l.path).resolve().as_posix() + # lib_path = Path(l.path).resolve().as_posix() - prev_name = self.get('asset_library') or self.library_name + prev_name = self.get("asset_library") or self.library_name - #print(l.name, prev_name) + # print(l.name, prev_name) - if (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') - + + # print('No library removed') + def set_dict(self, data, obj=None): - """"Recursive method to set all attribute from a dict to this instance""" + """ "Recursive method to set all attribute from a dict to this instance""" if obj is None: obj = self @@ -281,46 +318,45 @@ class AssetLibrary(PropertyGroup): # Make shure the input dict is not modidied data = data.copy() - #print(obj) + # print(obj) for key, value in data.items(): if isinstance(value, dict): - if 'name' in value: - setattr(obj, f'{key}_name', value.pop('name')) + if "name" in value: + setattr(obj, f"{key}_name", value.pop("name")) - #print('Nested value', getattr(obj, key)) + # 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': + if key == "id": value = str(value) - - elif key == 'custom_bundle_name': - if not 'use_custom_bundle_name' in data.values(): + + 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) + # print('set attr', key, value) setattr(obj, key, value) - #obj[key] = value - - else: - print(f'Prop {key} of {obj} not exist') + # obj[key] = value - self['bundle_directory'] = str(self.library_path) + 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["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': @@ -329,7 +365,7 @@ class AssetLibrary(PropertyGroup): # 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 @@ -337,22 +373,26 @@ class AssetLibrary(PropertyGroup): # self[key] = value def to_dict(self): - data = {p: getattr(self, p) for p in self.bl_rna.properties.keys() if p !='rna_type'} + 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'] + 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'] + 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''' + """Update the Blender Preference Filepaths tab with the addon libraries""" prefs = bpy.context.preferences name = self.library_name @@ -360,7 +400,6 @@ class AssetLibrary(PropertyGroup): 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() @@ -370,7 +409,7 @@ class AssetLibrary(PropertyGroup): # 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) @@ -380,19 +419,19 @@ class AssetLibrary(PropertyGroup): # Create the Asset Library Path lib = prefs.filepaths.asset_libraries.get(name) if not lib: - #print(f'Creating the lib {name}') + # 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 + self["asset_library"] = name lib.path = str(lib_path) - + @property def is_user(self): prefs = get_addon_prefs() @@ -403,24 +442,24 @@ class AssetLibrary(PropertyGroup): 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''' + 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.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='') + row.prop(boolean_data, boolean, text="") enabled = getattr(boolean_data, boolean) row = split.row(align=True) @@ -429,69 +468,72 @@ class AssetLibrary(PropertyGroup): if isinstance(data, str): row.label(text=data) else: - row.prop(data or self, prop, text='') - + 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.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 + row.operator("assetlib.diff", text="", icon="FILE_REFRESH").name = self.name - op = row.operator("assetlib.bundle", icon='MOD_BUILD', text='') + 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() + # box = layout.box() row = layout.row(align=True) - #row.use_property_split = False + # row.use_property_split = False - #row.alignment = 'LEFT' + # row.alignment = 'LEFT' icon = "DISCLOSURE_TRI_DOWN" if self.expand else "DISCLOSURE_TRI_RIGHT" - row.prop(self, 'expand', icon=icon, emboss=False, text='') + 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='') + 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 + row.operator( + "assetlib.remove_user_library", icon="X", text="", emboss=False + ).index = index else: - row.prop(self, 'use', text='') + row.prop(self, "use", text="") row.label(icon=ICONS[self.data_type]) - #row.label(text=self.name) + # 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() + 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') + 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 = col.row(align=True) - row = self.add_row(col, - prop="custom_bundle_name", - boolean="use_custom_bundle_name", - label='Custom Bundle Name') + 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 @@ -499,31 +541,32 @@ class AssetLibrary(PropertyGroup): 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', + 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') + # 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''' + """Util Class to merge multiple collections""" collections = [] @@ -531,17 +574,17 @@ class Collections: self.collections = collection for col in collection: - #print('Merge methods') + # print('Merge methods') for attr in dir(col): - if attr.startswith('_'): + if attr.startswith("_"): continue value = getattr(col, attr) - #if not callable(value): + # if not callable(value): # continue setattr(self, attr, value) - + def __contains__(self, item): if isinstance(item, str): return item in self.to_dict() @@ -556,12 +599,12 @@ class Collections: 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()} + 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()] @@ -576,11 +619,11 @@ class Collections: if not c: return item in self - + return list(c.values()).index(item) -#class AssetLibraryOptions(PropertyGroup): +# class AssetLibraryOptions(PropertyGroup): # pass @@ -593,78 +636,80 @@ class AssetLibraryPrefs(AddonPreferences): preview_modal = False add_asset_dict = {} - #action : bpy.props.PointerProperty(type=AssetLibraryPath) - #asset : bpy.props.PointerProperty(type=AssetLibraryPath) - #library_types = {} + # 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='') + 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') + 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) + env_libraries: CollectionProperty(type=AssetLibrary) + user_libraries: CollectionProperty(type=AssetLibrary) expand_settings: BoolProperty(default=False) - bundle_directory : StringProperty( - name="Path", - subtype='DIR_PATH', - default='', - update=update_all_library_path + bundle_directory: StringProperty( + name="Path", subtype="DIR_PATH", default="", update=update_all_library_path ) - config_directory : StringProperty( + config_directory: StringProperty( name="Config Path", - subtype='FILE_PATH', - default=str(RESOURCES_DIR/"asset_library_config.json"), - update=update_library_config + 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') - + print("Asset Library: Load Library Types") + LIBRARY_TYPES.clear() - library_type_files = list(LIBRARY_TYPE_DIR.glob('*.py')) + 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')) + library_type_files += list(user_LIBRARY_TYPE_DIR.glob("*.py")) for library_type_file in library_type_files: - if library_type_file.stem.startswith('_'): + if library_type_file.stem.startswith("_"): continue mod = import_module_from_path(library_type_file) - - #print(library_type_file) + # print(library_type_file) for name, obj in inspect.getmembers(mod): - if not inspect.isclass(obj): + if not inspect.isclass(obj): continue - #print(obj.__bases__) + # 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): + if obj is LibraryType or obj.name in (a.name for a in LIBRARY_TYPES): continue try: - print(f'Register Plugin {name}') + print(f"Register Plugin {name}") bpy.utils.register_class(obj) - setattr(LibraryTypes, norm_str(obj.name), bpy.props.PointerProperty(type=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(f"Could not register library_type {name}") print(e) - + def load_adapters(self): return @@ -676,52 +721,60 @@ class AssetLibraryPrefs(AddonPreferences): prefs = get_addon_prefs() layout = self.layout - #layout.use_property_split = True + # layout.use_property_split = True main_col = layout.column(align=False) box = main_col.box() row = box.row(align=True) icon = "DISCLOSURE_TRI_DOWN" if self.expand_settings else "DISCLOSURE_TRI_RIGHT" - row.prop(self, 'expand_settings', icon=icon, emboss=False, text='') - row.label(icon='PREFERENCES') - row.label(text='Settings') - #row.separator_spacer() + 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') + 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, 'use_single_path', text='Single Path') + col.prop(self, "bundle_directory", text="Bundle Directory") col.separator() - col.prop(self, 'library_type_directory') - col.prop(self, 'config_directory') + col.prop(self, "library_type_directory") + col.prop(self, "config_directory") col.separator() - #col.prop(self, 'template_info', text='Asset Description Template', icon='COPY_ID') + # col.prop(self, 'template_info', text='Asset Description Template', icon='COPY_ID') - #col.separator() + # 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.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.separator() - #col.prop(self, 'template_video', text='Template Video', icon='COPY_ID') - col.prop(self, 'video_player', text='Video Player') #icon='FILE_MOVIE' + # 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') + 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): + for ( + lib + ) in self.libraries: # list(self.env_libraries) + list(self.user_libraries): if lib.parent: continue @@ -729,46 +782,48 @@ class AssetLibraryPrefs(AddonPreferences): lib.draw(box) row = main_col.row() - row.alignment = 'RIGHT' - row.operator("assetlib.add_user_library", icon="ADD", text='', emboss=False) + row.alignment = "RIGHT" + row.operator("assetlib.add_user_library", icon="ADD", text="", emboss=False) classes = [ LibraryTypes, Adapters, - #ConformAssetLibrary, + # ConformAssetLibrary, AssetLibrary, AssetLibraryPrefs, ] + def register(): for cls in classes: bpy.utils.register_class(cls) - + prefs = get_addon_prefs() # Read Env and override preferences - bundle_dir = os.getenv('ASSETLIB_BUNDLE_DIR') + bundle_dir = os.getenv("ASSETLIB_BUNDLE_DIR") if bundle_dir: - prefs['bundle_directory'] = os.path.expandvars(bundle_dir) + prefs["bundle_directory"] = os.path.expandvars(bundle_dir) - config_dir = os.getenv('ASSETLIB_CONFIG_DIR') + config_dir = os.getenv("ASSETLIB_CONFIG_DIR") if config_dir: - prefs['config_directory'] = os.path.expandvars(config_dir) + prefs["config_directory"] = os.path.expandvars(config_dir) - LIBRARY_TYPE_DIR = os.getenv('ASSETLIB_LIBRARY_TYPE_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') + 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["adapter_directory"] = os.path.expandvars(ADAPTER_DIR) prefs.load_library_types() prefs.load_adapters() + def unregister(): for cls in reversed(classes + LIBRARY_TYPES): bpy.utils.unregister_class(cls) - LIBRARY_TYPES.clear() \ No newline at end of file + LIBRARY_TYPES.clear() diff --git a/tests/test_library_cache.py b/tests/test_library_cache.py index c8dcc9d..bcab2ab 100644 --- a/tests/test_library_cache.py +++ b/tests/test_library_cache.py @@ -1,4 +1,3 @@ - import bpy from pathlib import Path from asset_library.common.file_utils import read_file, write_file @@ -11,27 +10,24 @@ 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', {}) - + 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 = {".library_id": self.library.id, ".filepath": self.filepath} metadata.update(self.metadata) @@ -39,7 +35,7 @@ class AssetCache: @property def norm_name(self): - return self.name.replace(' ', '_').lower() + return self.name.replace(" ", "_").lower() def to_dict(self): return dict( @@ -49,23 +45,23 @@ class AssetCache: description=self.description, tags=self.tags, type=self.type, - name=self.name + name=self.name, ) def __str__(self): - return f'AssetCache(name={self.name}, type={self.type}, catalog={self.catalog})' + 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.filepath = data["filepath"] + self.modified = data.get("modified", time.time_ns()) self._data = [] - for asset_cache_data in data.get('assets', []): + for asset_cache_data in data.get("assets", []): self.add(asset_cache_data) def add(self, asset_cache_data): @@ -77,7 +73,7 @@ class FileCache: 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] + assets=[asset_cache.to_dict() for asset_cache in self], ) def __iter__(self): @@ -87,14 +83,14 @@ class FileCache: return self._data[key] def __str__(self): - return f'FileCache(filepath={self.filepath})' + 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.filepath = data["filepath"] self.operation = operation @@ -102,7 +98,7 @@ class LibraryCacheDiff: def __init__(self, filepath=None): self.filepath = filepath - self._data = [] + self._data = [] def add(self, asset_diff): asset_diff = AssetCacheDiff(self, asset_diff) @@ -111,23 +107,23 @@ class LibraryCacheDiff: 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}') - + 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''' + """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) @@ -138,13 +134,13 @@ class LibraryCacheDiff: class LibraryCache: def __init__(self, directory, id): - + self.directory = directory self.id = id self._data = [] @classmethod - def from_library(cls, library): + def from_library(cls, library): return cls(library.library_path, library.id) @property @@ -155,19 +151,19 @@ class LibraryCache: 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 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}') - + print(f"Read cache from {self.filepath}") + for file_cache_data in read_file(self.filepath): self.add(file_cache_data) @@ -178,7 +174,7 @@ class LibraryCache: if temp: filepath = self.tmp_filepath - print(f'Write cache file to {filepath}') + print(f"Write cache file to {filepath}") write_file(filepath, self._data) return filepath @@ -188,25 +184,28 @@ class LibraryCache: self._data.append(file_cache) def unflatten_cache(self, cache): - """ Return a new unflattten list of asset data + """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']) + cache.sort(key=lambda x: x["filepath"]) + groups = groupby(cache, key=lambda x: x["filepath"]) - keys = ['filepath', 'modified', 'library_id'] + keys = ["filepath", "modified", "library_id"] for _, asset_datas in groups: asset_datas = list(asset_datas) - #print(asset_datas[0]) + # 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] + 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) @@ -218,42 +217,59 @@ class LibraryCache: 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} + 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]] + 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') + 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') + 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') - + 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) - + cache_diff.set(assets_added + assets_removed + assets_modified) + if not len(LibraryCacheDiff): - print('No change in the library') + 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})' + return f"LibraryCache(library={self.library.name})" + print() -prefs = bpy.context.preferences.addons['asset_library'].preferences +prefs = bpy.context.preferences.addons["asset_library"].preferences library = prefs.env_libraries[0] @@ -264,7 +280,7 @@ print(data) print(library_cache[0][0]) -#library_cache.diff(library.library_type.fetch()) +# library_cache.diff(library.library_type.fetch()) -#print(library_cache[0]) +# print(library_cache[0])