black format

This commit is contained in:
Joseph HENRY 2026-01-07 16:05:47 +01:00
parent f7c125ae7b
commit 3d65bb6e4d
49 changed files with 2817 additions and 2280 deletions

View File

@ -23,8 +23,9 @@ 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.bl_utils import get_addon_prefs
from asset_library.common.functions import set_env_libraries
@ -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
@ -56,40 +57,26 @@ import os
# 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()
bpy.app.timers.register(load_handler, first_interval=1)
@ -99,5 +86,3 @@ def unregister() -> None:
for m in reversed(bl_modules):
m.unregister()

View File

@ -1,4 +1,3 @@
from asset_library.action import (
gui,
keymaps,
@ -10,7 +9,7 @@ from asset_library.action import (
# render_preview
)
if 'bpy' in locals():
if "bpy" in locals():
import importlib
importlib.reload(gui)
@ -24,10 +23,12 @@ if 'bpy' in locals():
import bpy
def register():
operators.register()
keymaps.register()
def unregister():
operators.unregister()
keymaps.unregister()

View File

@ -1,7 +1,6 @@
import sys
from pathlib import Path
# sys.path.append(str(Path(__file__).parents[3]))
from asset_library.action.concat_preview import mosaic_export
@ -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
@ -70,7 +76,7 @@ def render_preview(directory, asset_catalog, render_actions, publish_actions, re
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,57 +219,65 @@ 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
@ -262,30 +286,39 @@ 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('--')
if "--" in sys.argv:
index = sys.argv.index("--")
sys.argv = [sys.argv[index - 1], *sys.argv[index + 1 :]]
args = parser.parse_args()

View File

@ -1,22 +1,23 @@
import argparse
import bpy
import json
import sys
from pathlib import Path
# sys.path.append(str(Path(__file__).parents[3]))
from asset_library.common.bl_utils import (
get_preview,
)
def clear_asset(action_name='', use_fake_user=False):
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()
@ -28,20 +29,20 @@ def clear_asset(action_name='', use_fake_user=False):
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,
)
parser.add_argument("--action-name")
parser.add_argument("--use-fake-user", type=json.loads, default="false")
if __name__ == '__main__' :
parser = argparse.ArgumentParser(description='Add Comment To the tracker',
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument('--action-name')
parser.add_argument('--use-fake-user', type=json.loads, default='false')
if '--' in sys.argv :
index = sys.argv.index('--')
if "--" in sys.argv:
index = sys.argv.index("--")
sys.argv = [sys.argv[index - 1], *sys.argv[index + 1 :]]
args = parser.parse_args()

View File

@ -1,4 +1,3 @@
import bpy
import math
import numpy as np
@ -19,9 +18,11 @@ def alpha_to_color(pixels_data, color):
# print(new_pixels_data)#Dbg
return new_pixels_data
def create_array(height, width):
return np.zeros((height * width * 4), dtype=np.float32)
def read_pixels_data(img, source_height, source_width):
img_w, img_h = img.size
@ -43,6 +44,7 @@ def read_pixels_data(img, source_height, source_width):
return array.reshape(source_height, source_width, 4)
def create_final(output_name, pixels_data, final_height, final_width):
# print('output_name: ', output_name)
@ -58,23 +60,35 @@ def create_final(output_name, pixels_data, final_height, final_width):
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)
@ -92,21 +106,21 @@ def mosaic_export(
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)
if rows * columns < len(img_list):
raise AttributeError('Grid too small for number of images')
raise AttributeError("Grid too small for number of images")
src_w, src_h = img_list[0].size
final_w = src_w * columns
@ -130,17 +144,20 @@ def mosaic_export(
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}
""")
"""
)

View File

@ -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,6 +30,7 @@ def is_pose(action):
return True
def get_bone_visibility(data_path):
bone, prop = split_path(data_path)
@ -47,6 +41,7 @@ def get_bone_visibility(data_path):
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)])
@ -65,14 +60,21 @@ def get_keyframes(action, selected=False, includes=[]):
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:
@ -95,29 +97,32 @@ def reset_bone(bone, transform=True, custom_props=True):
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)
@ -138,14 +143,16 @@ def clean_action(action='', frame_start=0, frame_end=0, excludes=[], includes=[]
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
@ -162,14 +169,23 @@ def apply_anim(action_lib, ob, bones=[]):
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,17 +198,16 @@ 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)
@ -203,5 +218,5 @@ def apply_anim(action_lib, ob, bones=[]):
for window in bpy.context.window_manager.windows:
screen = window.screen
for area in screen.areas:
if area.type == 'GRAPH_EDITOR':
if area.type == "GRAPH_EDITOR":
area.tag_redraw()

View File

@ -1,4 +1,3 @@
import bpy
@ -6,12 +5,14 @@ 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.remove_assets", text="Remove Assets")
@ -21,29 +22,42 @@ def draw_context_menu(layout):
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.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(
"poselib.blend_pose_asset_for_keymap", text="Blend Pose"
).flipped = False
layout.operator(
"poselib.blend_pose_asset_for_keymap", text="Blend Pose (Flipped)"
).flipped = True
layout.separator()
layout.operator("poselib.pose_asset_select_bones", text="Select Bones").selected_side = 'CURRENT'
layout.operator("poselib.pose_asset_select_bones", text="Select Bones (Flipped)").selected_side = 'FLIPPED'
layout.operator("poselib.pose_asset_select_bones", text="Select Bones (Both)").selected_side = 'BOTH'
layout.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':
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")

View File

@ -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
@ -15,34 +14,49 @@ def register():
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)

View File

@ -26,13 +26,10 @@ from pprint import pprint
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 (
apply_anim,
@ -40,7 +37,7 @@ from asset_library.action.functions import(
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 (
@ -80,7 +77,7 @@ from asset_library.common.functions import (
# resync_lib,
get_active_library,
get_active_catalog,
asset_warning_callback
asset_warning_callback,
)
from asset_library.common.bl_utils import (
@ -95,7 +92,7 @@ from asset_library.common.bl_utils import (
# 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,8 +168,8 @@ 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:
@ -183,7 +180,11 @@ 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]:
@ -194,12 +195,13 @@ class ACTIONLIB_OT_replace_pose(Operator):
select_bones(
context.object,
context.asset_file_handle.local_id,
selected_side='BOTH',
toggle=False)
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))
@ -212,7 +214,7 @@ class ACTIONLIB_OT_replace_pose(Operator):
action = create_pose_asset_from_context(
context,
data['name'],
data["name"],
)
if not action:
self.report( # type: ignore
@ -226,33 +228,36 @@ 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
@ -263,20 +268,20 @@ class ACTIONLIB_OT_apply_anim(Operator):
# 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"}
"""
@ -477,8 +484,10 @@ class ACTIONLIB_OT_create_anim_asset(Operator):
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
@ -498,67 +507,71 @@ class ACTIONLIB_OT_create_anim_asset(Operator):
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,18 +633,18 @@ 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()
@ -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)
@ -688,7 +705,7 @@ class ACTIONLIB_OT_update_action_data(Operator):
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,29 +734,27 @@ 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]:
@ -747,14 +762,14 @@ class ACTIONLIB_OT_assign_rest_pose(Operator):
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'
bl_label = "Set Paths"
bl_description = "Open Containing File"
replace_local: BoolProperty(default=False)
@ -764,7 +779,7 @@ 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
@ -777,13 +792,11 @@ class ACTIONLIB_OT_open_blendfile(Operator):
blendfile=str(asset_path),
)
subprocess.Popen(cmd)
return {'FINISHED'}
return {"FINISHED"}
# 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,32 +868,40 @@ 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')
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')
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
@ -882,7 +916,16 @@ 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")
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):
@ -893,41 +936,43 @@ class ACTIONLIB_OT_store_anim_pose(Operator):
split = layout.split(factor=0.39, align=True)
# row = split.row(align=False)
# split.use_property_split = False
split.alignment = 'RIGHT'
split.alignment = "RIGHT"
split.label(text='Preview')
split.label(text="Preview")
sub = split.row(align=True)
sub.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')
sub.label(icon="BLANK1")
# layout.ui_units_x = 50
# 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)
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(self, "tags")
layout.prop(self, "description", text="Description")
# layout.prop(prefs, 'author', text='Author')
# layout.prop()
@ -941,7 +986,7 @@ class ACTIONLIB_OT_store_anim_pose(Operator):
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()
@ -975,8 +1020,7 @@ class ACTIONLIB_OT_store_anim_pose(Operator):
self.store_library = lib.name
# lib = self.current_library
self.tags = ''
self.tags = ""
# print(self, self.library_items)
catalog_item = lib.catalog.active_item
@ -991,7 +1035,7 @@ 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)
@ -1012,17 +1056,17 @@ class ACTIONLIB_OT_store_anim_pose(Operator):
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,15 +1074,15 @@ 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
@ -1050,21 +1094,21 @@ 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":
@ -1076,7 +1120,7 @@ class ACTIONLIB_OT_store_anim_pose(Operator):
pass
def refresh(self, area):
bpy.ops.asset.library_refresh({"area": area, 'region': area.regions[3]})
bpy.ops.asset.library_refresh({"area": area, "region": area.regions[3]})
# space_data.activate_asset_by_id(asset, deferred=deferred)
def execute(self, context: Context):
@ -1094,8 +1138,12 @@ class ACTIONLIB_OT_store_anim_pose(Operator):
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
@ -1128,7 +1176,7 @@ class ACTIONLIB_OT_store_anim_pose(Operator):
# 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,7 +1185,7 @@ 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_path.write_text(json.dumps(diff, indent=4))
@ -1146,7 +1194,7 @@ class ACTIONLIB_OT_store_anim_pose(Operator):
# 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"}
@ -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)

View File

@ -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(
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')
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

View File

@ -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]))
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,20 +25,20 @@ 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,
)
parser.add_argument("--src-name")
parser.add_argument("--dst-name")
if __name__ == '__main__' :
parser = argparse.ArgumentParser(description='Add Comment To the tracker',
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument('--src-name')
parser.add_argument('--dst-name')
if '--' in sys.argv :
index = sys.argv.index('--')
if "--" in sys.argv:
index = sys.argv.index("--")
sys.argv = [sys.argv[index - 1], *sys.argv[index + 1 :]]
args = parser.parse_args()

View File

@ -1,5 +1,3 @@
from bpy.types import PropertyGroup
@ -7,6 +5,11 @@ class Adapter(PropertyGroup):
# def __init__(self):
name = "Base Adapter"
# library = None
def to_dict(self):
return {p: getattr(self, p) for p in self.bl_rna.properties.keys() if p !='rna_type'}
return {
p: getattr(self, p)
for p in self.bl_rna.properties.keys()
if p != "rna_type"
}

View File

@ -1,4 +1,3 @@
from asset_library.collection import (
gui,
operators,
@ -7,7 +6,7 @@ from asset_library.collection import (
# create_collection_library,
)
if 'bpy' in locals():
if "bpy" in locals():
import importlib
importlib.reload(gui)
@ -18,10 +17,12 @@ if 'bpy' in locals():
import bpy
def register():
operators.register()
keymaps.register()
def unregister():
operators.unregister()
keymaps.unregister()

View File

@ -29,6 +29,7 @@ from asset_library.constants import ASSETLIB_FILENAME
]
"""
def build_collection_blends(path, categories=None, clean=True):
t0 = time()
@ -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']
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']):
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,10 +82,10 @@ 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
@ -93,31 +97,35 @@ 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('--')
if "--" in sys.argv:
index = sys.argv.index("--")
sys.argv = [sys.argv[index - 1], *sys.argv[index + 1 :]]
args = parser.parse_args()

View File

@ -44,10 +44,11 @@ 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
@ -56,75 +57,80 @@ def create_collection_json(path, source_directory):
# 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]
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,31 +138,44 @@ 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'
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('--')
if "--" in sys.argv:
index = sys.argv.index("--")
sys.argv = [sys.argv[index - 1], *sys.argv[index + 1 :]]
args = parser.parse_args()

View File

@ -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

View File

@ -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,9 +12,12 @@ 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)

View File

@ -14,12 +14,11 @@ 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:
@ -28,10 +27,10 @@ class ASSETLIB_OT_load_asset(Operator):
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)

View File

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

View File

@ -1,4 +1,3 @@
"""
Generic Blender functions
"""
@ -6,21 +5,23 @@ Generic Blender functions
from pathlib import Path
from fnmatch import fnmatch
from typing import Any, List, Iterable, Optional, Tuple
Datablock = Any
import bpy
from bpy_extras import asset_utils
from asset_library.constants import RESOURCES_DIR
# from asset_library.common.file_utils import no
from os.path import abspath
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 = []
@ -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,24 +49,37 @@ 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 = [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'''
@ -169,6 +193,7 @@ def tag_redraw(screen: bpy.types.Screen) -> None:
# return cmd
def norm_value(value):
if isinstance(value, (tuple, list)):
values = []
@ -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,17 +358,13 @@ 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}')
src = path.replace("####", f"{i:04d}")
dst = src.replace(src.split("_")[-1].split(".")[0], f"{i+offset:04d}")
shutil.copy2(src, dst)
def split_path(path):
try:
bone_name = path.split('["')[1].split('"]')[0]
@ -337,15 +373,14 @@ def split_path(path) :
try:
prop_name = path.split('["')[2].split('"]')[0]
except:
prop_name = path.split('.')[-1]
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 []
@ -356,7 +391,10 @@ def load_datablocks(src, names=None, type='objects', link=True, expr=None, asset
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):
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)]
@ -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,7 +454,7 @@ 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
@ -421,18 +462,21 @@ def load_col(filepath, name, link=True, override=True, rig_pattern=None, context
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,16 +488,19 @@ 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:
@ -463,7 +510,7 @@ def get_object_libraries(ob):
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 = []

View File

@ -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
@ -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
@ -79,7 +80,7 @@ class Catalog:
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"""
@ -89,13 +90,15 @@ class Catalog:
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
@ -103,9 +106,9 @@ class Catalog:
"""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')
raise Exception(f"Cannot write catalog {self} no filepath setted")
lines = ['VERSION 1', '']
lines = ["VERSION 1", ""]
catalog_items = list(self)
if sort:
@ -114,8 +117,8 @@ class Catalog:
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"""
@ -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):
@ -163,7 +166,7 @@ class Catalog:
return cat_item
def update(self, catalogs):
'Add or remove catalog entries if on the list given or not'
"Add or remove catalog entries if on the list given or not"
catalogs = set(catalogs) # Remove doubles
@ -171,9 +174,13 @@ class Catalog:
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)
@ -197,4 +204,4 @@ class Catalog:
return item in self
def __repr__(self):
return f'Catalog(filepath={self.filepath})'
return f"Catalog(filepath={self.filepath})"

View File

@ -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
@ -51,45 +55,59 @@ def import_module_from_path(path):
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 = 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 = 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)):
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
if ex_dir and is_exclude(d.name, ex_dir):
# skip folder with excluded name
continue
files += get_last_files(
d.path, pattern=pattern, only_matching=only_matching, ex_file=ex_file, ex_dir=ex_dir, keep=keep)
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,7 +203,7 @@ def copy_dir(src, dst, only_new=False, only_recent=False, excludes=['.*'], inclu
copy_file(src, dst, only_new=only_new, only_recent=only_recent)
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)]
@ -183,110 +216,115 @@ def copy_dir(src, dst, only_new=False, only_recent=False, excludes=['.*'], inclu
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'''
"""Open a filepath inside the os explorer"""
if platform.system() == 'Darwin': # macOS
cmd = ['open']
elif platform.system() == 'Windows': # Windows
cmd = ['explorer']
if platform.system() == "Darwin": # macOS
cmd = ["open"]
elif platform.system() == "Windows": # Windows
cmd = ["explorer"]
if select:
cmd += ['/select,']
cmd += ["/select,"]
else: # linux variants
cmd = ['xdg-open']
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):
@ -300,19 +338,22 @@ def synchronize(src, dst, only_new=False, only_recent=False, clear=False):
# 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)

View File

@ -11,6 +11,7 @@ import os
import re
import time
# from asset_library.constants import ASSETLIB_FILENAME
import inspect
from asset_library.common.file_utils import read_file
@ -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
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,10 +64,11 @@ 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
@ -72,18 +77,20 @@ def get_active_library():
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 = {}
@ -302,14 +309,15 @@ 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
@ -322,7 +330,8 @@ def clear_env_libraries():
prefs.env_libraries.clear()
'''
"""
env_libs = get_env_libraries()
paths = [Path(l['path']).resolve().as_posix() for n, l in env_libs.items()]
@ -331,10 +340,11 @@ 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'''
"""Read the environments variables and create the libraries"""
# from asset_library.prefs import AssetLibraryOptions
prefs = get_addon_prefs()
@ -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)
'''
"""

View File

@ -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()
@ -111,7 +108,7 @@ 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:
@ -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,7 +155,7 @@ class FileCache:
return self._data[key]
def __repr__(self):
return f'FileCache(filepath={self.filepath})'
return f"FileCache(filepath={self.filepath})"
class AssetCacheDiff:
@ -192,29 +189,46 @@ class LibraryCacheDiff:
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}
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,7 +242,7 @@ 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:
@ -248,7 +262,7 @@ class LibraryCache:
@property
def library_id(self):
return self.filepath.stem.split('.')[-1]
return self.filepath.stem.split(".")[-1]
# @property
# def filepath(self):
@ -260,7 +274,7 @@ class LibraryCache:
@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
@ -268,7 +282,7 @@ class LibraryCache:
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:
@ -341,19 +355,19 @@ class LibraryCache:
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
@ -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})"

View File

@ -1,4 +1,3 @@
import argparse
import fnmatch
import importlib.util
@ -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))

View File

@ -9,25 +9,26 @@ 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)
value = value.replace("_", " ")
value = value = re.sub(r"([a-z])([A-Z])", rf"\1{sep}\2", value)
value = value.replace(" ", sep)
if spec == 'u':
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
@ -37,16 +38,16 @@ class Template:
@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)
@ -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
@ -92,14 +93,14 @@ class Template:
# 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,7 +115,7 @@ 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())
@ -124,4 +125,4 @@ class Template:
# return pattern
def __repr__(self):
return f'Template({self.raw})'
return f"Template({self.raw})"

View File

@ -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'
PREVIEW_ASSETS_SCRIPT = MODULE_DIR / "common" / "preview_assets.py"
# ADD_ASSET_DICT = {}

View File

@ -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()

View File

@ -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)
@ -35,29 +38,31 @@ def bundle_library(source_directory, bundle_directory, template_info, thumbnail_
field_values = re.findall(re_pattern, rel_path)[0]
field_data = {k: v for k, v in zip(field_names, field_values)}
name = field_data.get('name', f.stem)
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'
@ -65,6 +70,7 @@ def bundle_library(source_directory, bundle_directory, template_info, thumbnail_
# print(cmd)
# subprocess.call(cmd)
@command
def bundle_blend(filepath, depth=0):
# print('Bundle Blend...')
@ -73,11 +79,11 @@ def bundle_blend(filepath, depth=0):
# 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)
@ -88,8 +94,8 @@ def bundle_blend(filepath, depth=0):
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
total_assets = len(asset_file_data)
@ -99,39 +105,37 @@ 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)
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)
@ -141,20 +145,22 @@ def bundle_blend(filepath, depth=0):
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('--')
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)

View File

@ -1,4 +1,3 @@
import bpy
from pathlib import Path
@ -23,7 +22,9 @@ def draw_context_menu(layout):
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)
@ -32,7 +33,7 @@ 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"""
layout.separator()
# layout.operator("actionlib.store_anim_pose", text='Add Action', icon='FILE_NEW')

View File

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

View File

@ -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,8 +16,8 @@ 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:
@ -24,10 +26,10 @@ class ASSETLIB_OT_open_blend_file(Operator):
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
@ -41,17 +43,17 @@ class ASSETLIB_OT_open_blend_file(Operator):
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)

90
gui.py
View File

@ -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,11 +114,13 @@ 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'}
@ -131,7 +130,7 @@ class ASSETLIB_PT_pose_library_editing(PoseLibraryPanel, asset_utils.AssetBrowse
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,16 +143,16 @@ 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']
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')
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):
@ -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,7 +208,7 @@ 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
row.operator("assetlib.bundle", icon="UV_SYNC_SELECT", text="").name = lib.name
# op
# op.clean = False
# op.only_recent = True
@ -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.
@ -285,7 +289,7 @@ classes = (
ASSETLIB_PT_pose_library_editing,
# ASSETLIB_PT_pose_library_usage,
ASSETLIB_MT_context_menu,
ASSETLIB_PT_libraries
ASSETLIB_PT_libraries,
)
@ -293,8 +297,12 @@ 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(
# name="Active Pose Asset",
@ -319,7 +327,9 @@ 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()

View File

@ -12,17 +12,24 @@ 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,
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,12 +54,13 @@ 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:

View File

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

View File

@ -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,7 +22,7 @@ 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()
@ -35,16 +33,18 @@ class Conform(ScanFolder):
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,26 +52,30 @@ 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()}
# Change the int in the template by string to allow format
# Change the int in the template by string to allow format
# target_template_file = re.sub(r'{(\d+)}', r'{cat\1}', self.target_template_file)
format_data = self.format_asset_data(asset_data)
# 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:
@ -82,18 +86,16 @@ class Conform(ScanFolder):
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
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)
@ -124,41 +124,47 @@ class Conform(ScanFolder):
# camera = scn.camera
data_type = self.data_type # asset_info['data_type']
asset_path = self.format_path(asset_info['filepath'])
asset_path = self.format_path(asset_info["filepath"])
# Check if a source video exists and if so copying it in the new directory
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:
@ -173,28 +179,29 @@ class Conform(ScanFolder):
# 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
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)
@ -205,7 +212,7 @@ class Conform(ScanFolder):
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()
@ -213,4 +220,6 @@ class Conform(ScanFolder):
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
)

View File

@ -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
@ -24,17 +22,14 @@ class CopyFolder(LibraryType):
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

View File

@ -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
@ -27,43 +25,43 @@ class Kitsu(LibraryType):
name = "Kitsu"
template_name: StringProperty()
template_file: StringProperty()
source_directory : StringProperty(subtype='DIR_PATH')
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'''
"""Connect to kitsu api using provided url, login and password"""
gazu = install_module('gazu')
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
@ -72,24 +70,26 @@ class Kitsu(LibraryType):
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')
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(
assets=[
dict(
catalog=catalog,
metadata=data.get('data', {}),
description=data['description'],
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'])
]
name=data["name"],
)
],
)
return asset_info
@ -100,29 +100,28 @@ 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...')
print("Generate previews...")
if cache in (None, ''):
if cache in (None, ""):
cache = self.fetch()
elif isinstance(cache, (Path, str)):
cache = self.read_cache(cache)
@ -130,7 +129,7 @@ class Kitsu(LibraryType):
# 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,21 +140,22 @@ 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
@ -164,12 +164,14 @@ class Kitsu(LibraryType):
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 = 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)
@ -178,12 +180,14 @@ class Kitsu(LibraryType):
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)
image_path = self.format_path(
self.target_template_image, asset_data, filepath=asset_path
)
# Force redo preview
# if asset.preview:
@ -191,7 +195,7 @@ class Kitsu(LibraryType):
# 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)
@ -204,12 +208,13 @@ class Kitsu(LibraryType):
# 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()
@ -217,53 +222,63 @@ class Kitsu(LibraryType):
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

View File

@ -1,11 +1,10 @@
# 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
@ -82,20 +81,28 @@ class LibraryType(PropertyGroup):
@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,25 +127,29 @@ 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"""
@ -153,9 +164,9 @@ class LibraryType(PropertyGroup):
)
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,8 +176,8 @@ 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:
@ -178,14 +189,14 @@ class LibraryType(PropertyGroup):
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,30 +206,30 @@ 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):
@ -230,8 +241,8 @@ class LibraryType(PropertyGroup):
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,11 +272,7 @@ 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):
@ -321,8 +328,8 @@ 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()
@ -332,15 +339,15 @@ class LibraryType(PropertyGroup):
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
@ -373,12 +380,17 @@ class LibraryType(PropertyGroup):
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()
@ -390,25 +402,27 @@ class LibraryType(PropertyGroup):
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
@ -572,16 +584,20 @@ class LibraryType(PropertyGroup):
blend_name = asset_cache.norm_name
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,55 +611,64 @@ 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")
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)
asset = getattr(bpy.data, self.data_types).new(
name=asset_cache.name
)
asset.asset_mark()
@ -662,12 +687,11 @@ class LibraryType(PropertyGroup):
self.set_asset_tags(asset, asset_cache)
self.set_asset_info(asset, asset_cache)
i += 1
# 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)
@ -678,7 +702,6 @@ class LibraryType(PropertyGroup):
# self.write_catalog(catalog_data)
catalog.write()
bpy.ops.wm.quit_blender()
# def unflatten_cache(self, cache):
@ -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))

View File

@ -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,18 +25,25 @@ 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')
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'
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'
default="high constrast, low constrast, medium constrast, midday, morning-afternoon, night, sunrise-sunset"
)
# blend_depth: IntProperty(default=1)
@ -64,32 +69,35 @@ 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', []),
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']
]
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(f"Fetch Assets for {self.library.name}")
print('self.asset_type: ', self.asset_type)
print("self.asset_type: ", self.asset_type)
url = f"https://api.polyhaven.com/assets?t={self.asset_type.lower()}"
# url2 = f"https://polyhaven.com/{self.asset_type.lower()}"
# url += "&future=true" if early_access else ""
@ -115,27 +123,26 @@ class PolyHaven(LibraryType):
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()])
# pp(res.json())
# pp(res2.json())
# print(res2)
# return asset_infos

View File

@ -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,7 +21,7 @@ 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()
@ -34,10 +32,10 @@ class ScanFolder(LibraryType):
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,10 +101,10 @@ 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)
@ -108,15 +112,15 @@ class ScanFolder(LibraryType):
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
@ -124,8 +128,8 @@ class ScanFolder(LibraryType):
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()
@ -140,50 +144,58 @@ 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")
print(f"Asset {asset_name} Already in Blend")
@ -192,7 +204,9 @@ class ScanFolder(LibraryType):
# 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()
@ -205,12 +219,11 @@ class ScanFolder(LibraryType):
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:
cache = self.read_cache()
@ -226,7 +239,7 @@ class ScanFolder(LibraryType):
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)
@ -237,21 +250,23 @@ class ScanFolder(LibraryType):
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()
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)
@ -260,17 +275,16 @@ class ScanFolder(LibraryType):
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)]
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
@ -282,9 +296,11 @@ class ScanFolder(LibraryType):
# 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)
@ -298,4 +314,3 @@ class ScanFolder(LibraryType):
getattr(bpy.data, self.data_types).remove(asset)
return cache

View File

@ -1,6 +1,5 @@
from typing import Set
# import shutil
from pathlib import Path
import subprocess
@ -11,11 +10,7 @@ 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)
import asset_library
@ -26,7 +21,8 @@ from asset_library.common.bl_utils import (
get_view3d_persp,
# 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,7 +47,7 @@ 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
@ -65,15 +61,19 @@ class ASSETLIB_OT_remove_assets(Operator):
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()
@ -90,7 +90,7 @@ class ASSETLIB_OT_remove_assets(Operator):
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,11 +126,13 @@ 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 = 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,
)
@ -137,11 +145,15 @@ 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():
@ -152,22 +164,32 @@ class ASSETLIB_OT_edit_data(Operator):
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,
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'
)]
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,12 +204,12 @@ 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()
@ -200,7 +222,7 @@ class ASSETLIB_OT_edit_data(Operator):
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,53 +235,58 @@ 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
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)
self.tags = ", ".join(tags)
# asset_path
self.old_catalog = catalog_ids[asset_handle.asset_data.catalog_id]['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_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)
@ -268,15 +295,14 @@ class ASSETLIB_OT_remove_user_library(Operator):
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,14 +310,14 @@ 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')
@ -307,21 +333,21 @@ class ASSETLIB_OT_open_blend(Operator):
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:
@ -331,20 +357,25 @@ class ASSETLIB_OT_set_paths(Operator):
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')
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):
@ -358,9 +389,9 @@ 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:
@ -368,9 +399,10 @@ class ASSETLIB_OT_bundle_library(Operator):
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,9 +412,10 @@ 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)
@ -398,31 +431,31 @@ class ASSETLIB_OT_bundle_library(Operator):
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)
@ -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,6 +516,7 @@ 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"
@ -491,15 +526,15 @@ class ASSETLIB_OT_make_custom_preview(Operator):
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 {'PASS_THROUGH'}
return {"PASS_THROUGH"}
def execute(self, context):
@ -509,12 +544,11 @@ 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')
prefs.previews.load(Path(img_path).stem, img_path, "IMAGE")
# img = bpy.data.images.load(context.scene.render.filepath)
# img.update()
# img.preview_ensure()
# Copy the image with a new name
# render = bpy.data.images['Render Result']
@ -534,8 +568,6 @@ class ASSETLIB_OT_make_custom_preview(Operator):
# image.preview.image_size = preview_size
# image.preview.image_pixels_float.foreach_set(pixels)
self.restore()
# self.is_running = False
@ -544,11 +576,11 @@ class ASSETLIB_OT_make_custom_preview(Operator):
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,7 +594,7 @@ 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)
@ -581,7 +613,7 @@ class ASSETLIB_OT_make_custom_preview(Operator):
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()
@ -592,12 +624,12 @@ 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()
@ -605,32 +637,34 @@ class ASSETLIB_OT_make_custom_preview(Operator):
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'),
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)),
])
(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)
@ -685,10 +722,11 @@ class ASSETLIB_OT_generate_previews(Operator):
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'
preview_blend = MODULE_DIR / "common" / "preview.blend"
script_path = Path(bpy.app.tempdir) / 'generate_previews.py'
script_code = dedent(f"""
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:
@ -745,33 +783,32 @@ class ASSETLIB_OT_play_preview(Operator):
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)
@ -781,11 +818,11 @@ class ASSETLIB_OT_synchronize(Operator):
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:
@ -794,7 +831,7 @@ class ASSETLIB_OT_synchronize(Operator):
for lib in libs:
if self.clean and Path(lib.path_local).exists():
pass
print('To check first')
print("To check first")
# shutil.rmtree(path_local)
if not lib.path_local:
@ -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,
@ -823,15 +861,17 @@ classes = (
ASSETLIB_OT_edit_data,
# 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
for cls in classes:
bpy.utils.register_class(cls)
def unregister():
for cls in reversed(classes):
bpy.utils.unregister_class(cls)

View File

@ -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()

View File

@ -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

View File

@ -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,19 +61,22 @@ 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
@ -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 "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 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)
@ -385,12 +402,12 @@ class POSELIB_OT_pose_asset_select_bones(Operator):
# 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",
@ -443,9 +462,12 @@ 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, 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
@ -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,6 +515,7 @@ 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"}
@ -496,7 +523,7 @@ class POSELIB_OT_apply_pose_asset_for_keymap(Operator):
_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 = "Apply Pose to Bones"
del _rna
flipped: BoolProperty(name="Flipped", default=False) # type: ignore
@ -514,19 +541,34 @@ class POSELIB_OT_apply_pose_asset_for_keymap(Operator):
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:
@ -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))
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)
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)

View File

@ -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,8 +300,11 @@ 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
) -> Optional[Action]:
"""Create Action asset from active object & selected bones."""
bones = context.selected_pose_bones_from_active_object
@ -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

View File

@ -35,15 +35,14 @@ def select_bones(arm_object: Object, action: Action, *, selected_side, toggle=Tr
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)
if pose_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()}")

View File

@ -1,19 +1,31 @@
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 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.constants import (
DATA_TYPES,
DATA_TYPE_ITEMS,
ICONS,
RESOURCES_DIR,
LIBRARY_TYPE_DIR,
LIBRARY_TYPES,
ADAPTERS,
)
from asset_library.common.file_utils import import_module_from_path, norm_str
from asset_library.common.bl_utils import get_addon_prefs
from asset_library.common.library_cache import LibraryCache
from asset_library.common.catalog import Catalog
# from asset_library.common.functions import get_catalog_path
from pathlib import Path
@ -22,19 +34,20 @@ 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:
# self['custom_bundle_directory'] = abspath(bpy.path.abspath(self.custom_bundle_directory))
@ -44,6 +57,7 @@ def update_library_path(self, context):
self.set_library_path()
def update_all_library_path(self, context):
# print('update_all_assetlib_paths')
@ -56,77 +70,97 @@ def update_all_library_path(self, context):
update_library_path(lib, context)
# lib.set_library_path()
def get_library_type_items(self, context):
# prefs = get_addon_prefs()
items = [('NONE', 'None', '', 0)]
items += [(norm_str(a.name, format=str.upper), a.name, "", i+1) for i, a in enumerate(LIBRARY_TYPES)]
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)]
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()
# 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)
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')
auto_bundle: BoolProperty(name="Auto Bundle", default=False)
expand: BoolProperty(name="Expand", default=False)
use: BoolProperty(name="Use", default=True, update=update_library_path)
data_type: EnumProperty(name="Type", items=DATA_TYPE_ITEMS, default="COLLECTION")
# template_image : StringProperty(default='', description='../{name}_image.png')
# template_video : StringProperty(default='', description='../{name}_video.mov')
# template_info : StringProperty(default='', description='../{name}_asset_info.json')
bundle_directory: StringProperty(
name="Bundle Directory",
subtype='DIR_PATH',
default=''
name="Bundle Directory", subtype="DIR_PATH", default=""
)
use_custom_bundle_directory: BoolProperty(default=False, update=update_library_path)
custom_bundle_directory: StringProperty(
name="Bundle Directory",
subtype='DIR_PATH',
default='',
update=update_library_path
subtype="DIR_PATH",
default="",
update=update_library_path,
)
# use_merge : BoolProperty(default=False, update=update_library_path)
use_custom_bundle_name: BoolProperty(default=False, update=update_library_path)
custom_bundle_name : StringProperty(name='Merge Name', update=update_library_path)
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)
@ -134,8 +168,8 @@ class AssetLibrary(PropertyGroup):
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,7 +178,6 @@ 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)
@ -172,7 +205,11 @@ 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)]
return [
l
for l in prefs.libraries
if l != self and (l.library_path == self.library_path)
]
@property
def child_libraries(self):
@ -182,9 +219,9 @@ class AssetLibrary(PropertyGroup):
@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):
@ -258,11 +295,11 @@ class AssetLibrary(PropertyGroup):
for l in reversed(libs):
# 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)
if (l.name == prev_name):
if l.name == prev_name:
index = list(libs).index(l)
try:
bpy.ops.preferences.asset_library_remove(index=index)
@ -286,18 +323,18 @@ class AssetLibrary(PropertyGroup):
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))
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):
@ -309,19 +346,18 @@ class AssetLibrary(PropertyGroup):
# obj[key] = value
else:
print(f'Prop {key} of {obj} not exist')
print(f"Prop {key} of {obj} not exist")
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
# self.library_type_name = data['library_type']
# if not self.library_type:
# print(f"No library_type named {data['library_type']}")
# return
# for key, value in data.items():
# if key == 'options':
# for k, v in data['options'].items():
@ -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()
@ -390,7 +429,7 @@ class AssetLibrary(PropertyGroup):
lib.name = name
self['asset_library'] = name
self["asset_library"] = name
lib.path = str(lib_path)
@property
@ -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,20 +468,19 @@ 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)
@ -456,42 +494,46 @@ class AssetLibrary(PropertyGroup):
# 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)
subrow = row.row(align=True)
subrow.alignment = 'LEFT'
subrow.prop(self, 'expand', emboss=False, text=self.name)
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 = self.add_row(col,
row = self.add_row(
col,
prop="custom_bundle_name",
boolean="use_custom_bundle_name",
label='Custom Bundle Name')
label="Custom Bundle Name",
)
row.enabled = not self.use_custom_bundle_directory
@ -499,9 +541,11 @@ class AssetLibrary(PropertyGroup):
if self.use_custom_bundle_directory:
prop = "custom_bundle_directory"
self.add_row(col, prop=prop,
self.add_row(
col,
prop=prop,
boolean="use_custom_bundle_directory",
label='Custom Bundle Directory',
label="Custom Bundle Directory",
)
col.prop(self, "blend_depth")
@ -521,9 +565,8 @@ class AssetLibrary(PropertyGroup):
col.separator()
class Collections:
'''Util Class to merge multiple collections'''
"""Util Class to merge multiple collections"""
collections = []
@ -533,7 +576,7 @@ class Collections:
for col in collection:
# print('Merge methods')
for attr in dir(col):
if attr.startswith('_'):
if attr.startswith("_"):
continue
value = getattr(col, attr)
@ -598,49 +641,47 @@ class AssetLibraryPrefs(AddonPreferences):
# 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)
expand_settings: BoolProperty(default=False)
bundle_directory: StringProperty(
name="Path",
subtype='DIR_PATH',
default='',
update=update_all_library_path
name="Path", subtype="DIR_PATH", default="", update=update_all_library_path
)
config_directory: StringProperty(
name="Config Path",
subtype='FILE_PATH',
subtype="FILE_PATH",
default=str(RESOURCES_DIR / "asset_library_config.json"),
update=update_library_config
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)
for name, obj in inspect.getmembers(mod):
@ -656,13 +697,17 @@ class AssetLibraryPrefs(AddonPreferences):
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):
@ -683,25 +728,25 @@ class AssetLibraryPrefs(AddonPreferences):
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.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, "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()
@ -710,18 +755,26 @@ class AssetLibraryPrefs(AddonPreferences):
# 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, "image_player", text="Image Player"
) # icon='OUTLINER_OB_IMAGE'
# col.separator()
# col.prop(self, 'template_video', text='Template Video', icon='COPY_ID')
col.prop(self, 'video_player', text='Video Player') #icon='FILE_MOVIE'
col.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,8 +782,8 @@ 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 = [
@ -741,6 +794,7 @@ classes = [
AssetLibraryPrefs,
]
def register():
for cls in classes:
bpy.utils.register_class(cls)
@ -748,25 +802,26 @@ def register():
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)
prefs["library_type_directory"] = os.path.expandvars(LIBRARY_TYPE_DIR)
ADAPTER_DIR = os.getenv('ASSETLIB_ADAPTER_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)

View File

@ -1,4 +1,3 @@
import bpy
from pathlib import Path
from asset_library.common.file_utils import read_file, write_file
@ -14,13 +13,13 @@ class AssetCache:
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):
@ -28,10 +27,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)
@ -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
@ -113,7 +109,7 @@ class LibraryCacheDiff:
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)
@ -121,7 +117,7 @@ class LibraryCacheDiff:
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)
@ -158,7 +154,7 @@ class LibraryCache:
@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
@ -166,7 +162,7 @@ class LibraryCache:
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
@ -195,10 +191,10 @@ class LibraryCache:
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)
@ -206,7 +202,10 @@ class LibraryCache:
# 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["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,24 +217,40 @@ 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)
if not len(LibraryCacheDiff):
print('No change in the library')
print("No change in the library")
return cache_diff
@ -249,11 +264,12 @@ class LibraryCache:
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]