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

@ -2,7 +2,7 @@
"""
Extending features of the Asset Browser for a studio use.
Extending features of the Asset Browser for a studio use.
"""
bl_info = {
@ -16,16 +16,17 @@ bl_info = {
"category": "Animation",
}
#from typing import List, Tuple
# from typing import List, Tuple
from asset_library import pose
from asset_library import action
from asset_library import collection
from asset_library import file
from asset_library import (gui, keymaps, preferences, operators)
from asset_library import gui, keymaps, preferences, operators
from asset_library import constants
#from asset_library.common.library_type import LibraryType
# from asset_library.common.library_type import LibraryType
from asset_library.common.bl_utils import get_addon_prefs
from asset_library.common.functions import set_env_libraries
from asset_library.common.template import Template
@ -33,7 +34,7 @@ from asset_library.common.template import Template
import re
if 'bpy' in locals():
if "bpy" in locals():
print("Reload Addon Asset Library")
import importlib
@ -54,41 +55,27 @@ import bpy
import os
#addon_keymaps: List[Tuple[bpy.types.KeyMap, bpy.types.KeyMapItem]] = []
# addon_keymaps: List[Tuple[bpy.types.KeyMap, bpy.types.KeyMapItem]] = []
bl_modules = (
operators,
pose,
action,
collection,
file,
keymaps,
gui,
preferences
)
bl_modules = (operators, pose, action, collection, file, keymaps, gui, preferences)
def load_handler():
print('load_handler')
print("load_handler")
set_env_libraries()
bpy.ops.assetlib.set_paths(all=True)
if not bpy.app.background:
bpy.ops.assetlib.bundle(blocking=False, mode='AUTO_BUNDLE')
bpy.ops.assetlib.bundle(blocking=False, mode="AUTO_BUNDLE")
def register() -> None:
for m in bl_modules:
m.register()
#prefs = get_addon_prefs()
# prefs = get_addon_prefs()
bpy.app.timers.register(load_handler, first_interval=1)
@ -99,5 +86,3 @@ def unregister() -> None:
for m in reversed(bl_modules):
m.unregister()

View File

@ -1,4 +1,3 @@
from asset_library.action import (
gui,
keymaps,
@ -7,10 +6,10 @@ from asset_library.action import (
operators,
properties,
rename_pose,
#render_preview
)
# render_preview
)
if 'bpy' in locals():
if "bpy" in locals():
import importlib
importlib.reload(gui)
@ -20,14 +19,16 @@ if 'bpy' in locals():
importlib.reload(operators)
importlib.reload(properties)
importlib.reload(rename_pose)
#importlib.reload(render_preview)
# importlib.reload(render_preview)
import bpy
def register():
operators.register()
keymaps.register()
def unregister():
operators.unregister()
keymaps.unregister()
keymaps.unregister()

View File

@ -1,8 +1,7 @@
import sys
from pathlib import Path
#sys.path.append(str(Path(__file__).parents[3]))
# sys.path.append(str(Path(__file__).parents[3]))
from asset_library.action.concat_preview import mosaic_export
from asset_library.common.file_utils import open_file
@ -19,17 +18,19 @@ import subprocess
from tempfile import gettempdir
def rm_tree(pth):
pth = Path(pth)
for child in pth.glob('*'):
for child in pth.glob("*"):
if child.is_file():
child.unlink()
else:
rm_tree(child)
pth.rmdir()
def render_preview(directory, asset_catalog, render_actions, publish_actions, remove_folder):
def render_preview(
directory, asset_catalog, render_actions, publish_actions, remove_folder
):
scn = bpy.context.scene
rnd = bpy.context.scene.render
@ -39,27 +40,32 @@ def render_preview(directory, asset_catalog, render_actions, publish_actions, re
blendfile = Path(bpy.data.filepath)
asset_catalog_data = read_catalog(asset_catalog)
anim_render_dir = Path(gettempdir()) / 'actionlib_render' #/tmp/actionlib_render. Removed at the end
anim_render_dir = (
Path(gettempdir()) / "actionlib_render"
) # /tmp/actionlib_render. Removed at the end
anim_render_dir.mkdir(exist_ok=True, parents=True)
preview_render_dir = Path(directory) / 'preview'
preview_render_dir = Path(directory) / "preview"
if preview_render_dir.exists() and remove_folder:
rm_tree(preview_render_dir)
preview_render_dir.mkdir(exist_ok=True, parents=True)
for i in ('anim', 'pose'):
for i in ("anim", "pose"):
Path(preview_render_dir / i).mkdir(exist_ok=True, parents=True)
for f in preview_render_dir.rglob('*'):
for f in preview_render_dir.rglob("*"):
if f.is_dir():
print(f'{f} is dir. Skipped.')
print(f"{f} is dir. Skipped.")
continue
if all(i not in f.parts for i in ('anim', 'pose')) and f.parent.parts[-1] != 'preview':
print(f'{f} is out of pipe. Approved or Rtk pictures. Skipped.')
if (
all(i not in f.parts for i in ("anim", "pose"))
and f.parent.parts[-1] != "preview"
):
print(f"{f} is out of pipe. Approved or Rtk pictures. Skipped.")
continue
if not any(f.stem.endswith(a) for a in publish_actions):
print(f'{str(f)} not in publish actions anymore. Removing...')
print(f"{str(f)} not in publish actions anymore. Removing...")
f.unlink()
# Set Scene
@ -68,9 +74,9 @@ def render_preview(directory, asset_catalog, render_actions, publish_actions, re
scn.use_preview_range = True
scn.eevee.use_gtao = True
scn.tool_settings.use_keyframe_insert_auto = False
# Render Setting
rnd.engine = 'BLENDER_EEVEE'
rnd.engine = "BLENDER_EEVEE"
rnd.use_simplify = False
rnd.use_stamp_date = True
rnd.use_stamp_time = True
@ -89,7 +95,7 @@ def render_preview(directory, asset_catalog, render_actions, publish_actions, re
rnd.use_stamp = True
rnd.stamp_font_size = 16
rnd.use_stamp_labels = False
rnd.image_settings.file_format = 'JPEG'
rnd.image_settings.file_format = "JPEG"
# Viewport Look
# ----------
@ -108,94 +114,104 @@ def render_preview(directory, asset_catalog, render_actions, publish_actions, re
"""
# Cycles Mat Shading
for a in bpy.context.screen.areas:
if a.type == 'VIEW_3D':
if a.type == "VIEW_3D":
a.spaces[0].overlay.show_overlays = False
a.spaces[0].region_3d.view_perspective = 'CAMERA'
a.spaces[0].region_3d.view_perspective = "CAMERA"
a.spaces[0].shading.show_cavity = True
a.spaces[0].shading.cavity_type = 'WORLD'
a.spaces[0].shading.cavity_type = "WORLD"
a.spaces[0].shading.cavity_ridge_factor = 0.75
a.spaces[0].shading.cavity_valley_factor = 1.0
# Add Subsurf
# -----------
deform_ob = [m.object for o in scn.objects \
for m in o.modifiers if m.type == 'MESH_DEFORM'
deform_ob = [
m.object for o in scn.objects for m in o.modifiers if m.type == "MESH_DEFORM"
]
deform_ob += [m.target for o in scn.objects \
for m in o.modifiers if m.type == 'SURFACE_DEFORM'
deform_ob += [
m.target for o in scn.objects for m in o.modifiers if m.type == "SURFACE_DEFORM"
]
objects = [o for o in bpy.context.scene.objects if (o.type == 'MESH'
and o not in deform_ob and o not in bpy.context.scene.collection.objects[:])
objects = [
o
for o in bpy.context.scene.objects
if (
o.type == "MESH"
and o not in deform_ob
and o not in bpy.context.scene.collection.objects[:]
)
]
for o in objects:
subsurf = False
for m in o.modifiers:
if m.type == 'SUBSURF':
if m.type == "SUBSURF":
m.show_viewport = m.show_render
m.levels = m.render_levels
subsurf = True
break
if not subsurf:
subsurf = o.modifiers.new('', 'SUBSURF')
subsurf = o.modifiers.new("", "SUBSURF")
subsurf.show_viewport = subsurf.show_render
subsurf.levels = subsurf.render_levels
# Loop through action and render
# ------------------------------
rig = next((o for o in scn.objects if o.type == 'ARMATURE'), None)
rig = next((o for o in scn.objects if o.type == "ARMATURE"), None)
# actions = [a for a in bpy.data.actions if a.asset_data]
rig.animation_data_create()
for action_name in render_actions:
action = bpy.data.actions.get(action_name)
if not action:
print(f'\'{action_name}\' not found.')
print(f"'{action_name}' not found.")
continue
print(f"-- Current --: {action.name}")
rnd.stamp_note_text = '{type} : {pose_name}'
rnd.stamp_note_text = "{type} : {pose_name}"
action_data = action.asset_data
if 'camera' not in action_data.keys():
if "camera" not in action_data.keys():
report.append(f"'{action.name}' has no CameraData.")
continue
catalog_name = next((v['name'] for v in asset_catalog_data.values() if action_data.catalog_id == v['id']), None)
pose_name = '/'.join([*catalog_name.split('-'), action.name])
filename = bpy.path.clean_name(f'{catalog_name}_{action.name}')
ext = 'jpg'
catalog_name = next(
(
v["name"]
for v in asset_catalog_data.values()
if action_data.catalog_id == v["id"]
),
None,
)
pose_name = "/".join([*catalog_name.split("-"), action.name])
filename = bpy.path.clean_name(f"{catalog_name}_{action.name}")
ext = "jpg"
rig.animation_data.action = None
bpy.context.view_layer.update()
for b in rig.pose.bones:
if re.match('^[A-Z]+\.', b.name):
if re.match("^[A-Z]+\.", b.name):
continue
reset_bone(b)
rest_pose = None
if isinstance(action.asset_data.get('rest_pose'), str):
rest_pose = bpy.data.actions.get(action.asset_data['rest_pose'])
if isinstance(action.asset_data.get("rest_pose"), str):
rest_pose = bpy.data.actions.get(action.asset_data["rest_pose"])
rig.animation_data.action = rest_pose
bpy.context.view_layer.update()
rig.animation_data.action = action
if 'camera' in action.asset_data.keys():
action_cam = bpy.data.objects.get(action.asset_data['camera'], '')
if "camera" in action.asset_data.keys():
action_cam = bpy.data.objects.get(action.asset_data["camera"], "")
if action_cam:
scn.camera = action_cam
# Is Anim
if not action_data['is_single_frame'] or 'anim' in action_data.tags.keys():
if not action_data["is_single_frame"] or "anim" in action_data.tags.keys():
keyframes = get_keyframes(action)
if not keyframes:
continue
@ -203,58 +219,66 @@ def render_preview(directory, asset_catalog, render_actions, publish_actions, re
anim_end = keyframes[-1]
if anim_start < scn.frame_start:
report.append(f"Issue found for '{action.name}'. Has keyframes before 'Start Frame'.")
report.append(
f"Issue found for '{action.name}'. Has keyframes before 'Start Frame'."
)
continue
scn.frame_preview_start = anim_start
scn.frame_preview_end = anim_end
rnd.stamp_note_text = rnd.stamp_note_text.format(
type='ANIM',
type="ANIM",
pose_name=pose_name,
)
rnd.filepath = f'{str(anim_render_dir)}/{filename}_####.{ext}'
rnd.filepath = f"{str(anim_render_dir)}/{filename}_####.{ext}"
bpy.ops.render.opengl(animation=True)
ffmpeg_cmd = [
'ffmpeg', '-y',
'-start_number', f'{anim_start:04d}',
'-i', rnd.filepath.replace('####', '%04d'),
'-c:v', 'libx264',
str((preview_render_dir/'anim'/filename).with_suffix('.mov')),
"ffmpeg",
"-y",
"-start_number",
f"{anim_start:04d}",
"-i",
rnd.filepath.replace("####", "%04d"),
"-c:v",
"libx264",
str((preview_render_dir / "anim" / filename).with_suffix(".mov")),
]
subprocess.call(ffmpeg_cmd)
# Is Pose
elif action_data['is_single_frame'] or 'pose' in action_data.tags.keys():
elif action_data["is_single_frame"] or "pose" in action_data.tags.keys():
scn.frame_preview_start = scn.frame_preview_end = scn.frame_start
rnd.stamp_note_text = rnd.stamp_note_text.format(
type='POSE',
type="POSE",
pose_name=pose_name,
)
rnd.filepath = f'{str(preview_render_dir)}/pose/{filename}_####.{ext}'
rnd.filepath = f"{str(preview_render_dir)}/pose/{filename}_####.{ext}"
bpy.ops.render.opengl(animation=True)
filename = rnd.filepath.replace('####', f'{scn.frame_preview_end:04d}')
Path(filename).rename(re.sub('_[0-9]{4}.', '.', filename))
filename = rnd.filepath.replace("####", f"{scn.frame_preview_end:04d}")
Path(filename).rename(re.sub("_[0-9]{4}.", ".", filename))
shutil.rmtree(anim_render_dir)
# Report
# ------
if report:
report_file = blendfile.parent / Path(f'{blendfile.stem}report').with_suffix('.txt')
report_file = blendfile.parent / Path(f"{blendfile.stem}report").with_suffix(
".txt"
)
if not report_file.exists():
report_file.touch(exist_ok=False)
report_file.write_text('-')
report_file.write_text('\n'.join(report))
report_file.write_text("-")
report_file.write_text("\n".join(report))
result = report_file
else:
@ -262,31 +286,40 @@ def render_preview(directory, asset_catalog, render_actions, publish_actions, re
open_file(result)
files = [str(f) for f in sorted((preview_render_dir/'pose').glob('*.jpg'))]
files = [str(f) for f in sorted((preview_render_dir / "pose").glob("*.jpg"))]
mosaic_export(
files=files, catalog_data=asset_catalog_data,
row=2, columns=2, auto_calculate=True,
bg_color=(0.18, 0.18, 0.18,), resize_output=100
files=files,
catalog_data=asset_catalog_data,
row=2,
columns=2,
auto_calculate=True,
bg_color=(
0.18,
0.18,
0.18,
),
resize_output=100,
)
bpy.ops.wm.quit_blender()
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Add Comment To the tracker",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
if __name__ == '__main__' :
parser = argparse.ArgumentParser(description='Add Comment To the tracker',
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument("--directory")
parser.add_argument("--asset-catalog")
parser.add_argument("--render-actions", nargs="+")
parser.add_argument("--publish-actions", nargs="+")
parser.add_argument("--remove-folder", type=json.loads, default="false")
parser.add_argument('--directory')
parser.add_argument('--asset-catalog')
parser.add_argument('--render-actions', nargs='+')
parser.add_argument('--publish-actions', nargs='+')
parser.add_argument('--remove-folder', type=json.loads, default='false')
if '--' in sys.argv :
index = sys.argv.index('--')
sys.argv = [sys.argv[index-1], *sys.argv[index+1:]]
if "--" in sys.argv:
index = sys.argv.index("--")
sys.argv = [sys.argv[index - 1], *sys.argv[index + 1 :]]
args = parser.parse_args()
render_preview(**vars(args))

View File

@ -1,25 +1,26 @@
import argparse
import bpy
import json
import sys
from pathlib import Path
#sys.path.append(str(Path(__file__).parents[3]))
# sys.path.append(str(Path(__file__).parents[3]))
from asset_library.common.bl_utils import (
get_preview,
)
def clear_asset(action_name='', use_fake_user=False):
def clear_asset(action_name="", use_fake_user=False):
scn = bpy.context.scene
action = bpy.data.actions.get(action_name)
if not action:
print(f'No {action_name} not found.')
print(f"No {action_name} not found.")
bpy.ops.wm.quit_blender()
action.asset_clear()
action.asset_clear()
if use_fake_user:
action.use_fake_user = True
else:
@ -27,22 +28,22 @@ def clear_asset(action_name='', use_fake_user=False):
if preview:
preview.unlink()
bpy.data.actions.remove(action)
bpy.ops.wm.save_mainfile(
filepath=bpy.data.filepath, compress=True, exit=True
bpy.ops.wm.save_mainfile(filepath=bpy.data.filepath, compress=True, exit=True)
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Add Comment To the tracker",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
if __name__ == '__main__' :
parser = argparse.ArgumentParser(description='Add Comment To the tracker',
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument('--action-name')
parser.add_argument('--use-fake-user', type=json.loads, default='false')
parser.add_argument("--action-name")
parser.add_argument("--use-fake-user", type=json.loads, default="false")
if '--' in sys.argv :
index = sys.argv.index('--')
sys.argv = [sys.argv[index-1], *sys.argv[index+1:]]
if "--" in sys.argv:
index = sys.argv.index("--")
sys.argv = [sys.argv[index - 1], *sys.argv[index + 1 :]]
args = parser.parse_args()
clear_asset(**vars(args))

View File

@ -1,4 +1,3 @@
import bpy
import math
import numpy as np
@ -10,75 +9,90 @@ def alpha_to_color(pixels_data, color):
"""Convert Alpha to WhiteBG"""
new_pixels_data = []
for i in pixels_data:
height, width, array_d = i.shape
mask = i[:,:,3:]
background = np.array([color[0], color[1], color[2] ,1], dtype=np.float32)
background = np.tile(background, (height*width))
background = np.reshape(background, (height,width,4))
height, width, array_d = i.shape
mask = i[:, :, 3:]
background = np.array([color[0], color[1], color[2], 1], dtype=np.float32)
background = np.tile(background, (height * width))
background = np.reshape(background, (height, width, 4))
new_pixels_data.append(i * mask + background * (1 - mask))
# print(new_pixels_data)#Dbg
return new_pixels_data
def create_array(height, width):
return np.zeros((height*width*4), dtype=np.float32)
return np.zeros((height * width * 4), dtype=np.float32)
def read_pixels_data(img, source_height, source_width):
img_w, img_h = img.size
if img_w != source_width :
scale = abs(img_w/source_width)
img.scale(int(img_w/scale), int(img_h/scale))
if img_w != source_width:
scale = abs(img_w / source_width)
img.scale(int(img_w / scale), int(img_h / scale))
img_w, img_h = img.size
array = create_array(img_h, img_w)
img.pixels.foreach_get(array)
array = array.reshape(img_h, img_w, 4)
if array.shape[0] != source_height:
#print('ARRAY SHAPE', array.shape[:], source_height)
missing_height = int(abs(source_height-img_h)/2)
# print('ARRAY SHAPE', array.shape[:], source_height)
missing_height = int(abs(source_height - img_h) / 2)
empty_array = create_array(missing_height, source_width)
empty_array = empty_array.reshape(missing_height, source_width, 4)
array = np.vstack((empty_array, array, empty_array))
return array.reshape(source_height, source_width, 4)
def create_final(output_name, pixels_data, final_height, final_width):
#print('output_name: ', output_name)
# print('output_name: ', output_name)
new_img = bpy.data.images.get(output_name)
if new_img:
bpy.data.images.remove(new_img)
new_img = bpy.data.images.new(output_name, final_width, final_height)
new_img.generated_color=(0,0,0,0)
new_img.generated_color = (0, 0, 0, 0)
#print('pixels_data: ', pixels_data)
# print('pixels_data: ', pixels_data)
new_img.pixels.foreach_set(pixels_data)
return new_img
def guess_input_format(img_list):
for i in img_list:
if i.size[0] == i.size[1]:
return i.size
def format_files(files, catalog_data):
img_dict = {}
for k, v in catalog_data.items():
if '/' not in k:
if "/" not in k:
continue
img_dict[v['name']] = [f for f in files if v['name'] in f]
img_dict[v["name"]] = [f for f in files if v["name"] in f]
return img_dict
def mosaic_export(
files, catalog_data, row=2, columns=2, auto_calculate=True,
bg_color=(0.18, 0.18, 0.18,), resize_output=100,
files,
catalog_data,
row=2,
columns=2,
auto_calculate=True,
bg_color=(
0.18,
0.18,
0.18,
),
resize_output=100,
):
img_dict = format_files(files, catalog_data)
for cat, files_list in img_dict.items():
if not files_list:
@ -86,61 +100,64 @@ def mosaic_export(
for i in bpy.data.images:
bpy.data.images.remove(i)
img_list = []
chars = Path(files_list[0]).parts[-4]
output_dir = str(Path(files_list[0]).parent.parent)
ext = 'jpg'
output_name = f'{chars}_{cat}.{ext}'
ext = "jpg"
output_name = f"{chars}_{cat}.{ext}"
for img in files_list:
img_list.append(bpy.data.images.load(img, check_existing=True))
for i in img_list:
i.colorspace_settings.name = 'Raw'
i.colorspace_settings.name = "Raw"
if auto_calculate:
rows = int(math.sqrt(len(img_list)))
columns = math.ceil(len(img_list)/rows)
columns = math.ceil(len(img_list) / rows)
if rows * columns < len(img_list):
raise AttributeError("Grid too small for number of images")
if rows*columns < len(img_list):
raise AttributeError('Grid too small for number of images')
src_w, src_h = img_list[0].size
final_w = src_w * columns
final_h = src_h * rows
img_pixels = [read_pixels_data(img, src_h, src_w) for img in img_list]
#Check if there is enough "data" to create an horizontal stack
# Check if there is enough "data" to create an horizontal stack
##It not, create empty array
h_stack = []
total_len = rows*columns
total_len = rows * columns
if len(img_pixels) < total_len:
for i in range(total_len-len(img_pixels)):
for i in range(total_len - len(img_pixels)):
img_pixels.append(create_array(src_h, src_w).reshape(src_h, src_w, 4))
img_pixels = alpha_to_color(img_pixels, bg_color)
for i in range(0,len(img_pixels),columns):
h_stack.append(np.hstack(img_pixels[i:i+columns]))
for i in range(0, len(img_pixels), columns):
h_stack.append(np.hstack(img_pixels[i : i + columns]))
if rows > 1:
combined_stack = np.vstack(h_stack[::-1])
else:
combined_stack = np.hstack((h_stack[:]))
combined_img = create_final(output_name, combined_stack.flatten(), final_h, final_w)
combined_img = create_final(
output_name, combined_stack.flatten(), final_h, final_w
)
if resize_output != 100:
w, h = combined_img.size
combined_img.scale(w*(resize_output*.01), h*(resize_output*.01))
combined_img.scale(w * (resize_output * 0.01), h * (resize_output * 0.01))
combined_img.filepath_raw = '/'.join([output_dir, output_name])
combined_img.file_format = 'JPEG'
combined_img.filepath_raw = "/".join([output_dir, output_name])
combined_img.file_format = "JPEG"
combined_img.save()
print(f"""
print(
f"""
Image saved: {combined_img.filepath_raw}
""")
"""
)

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,16 +30,18 @@ def is_pose(action):
return True
def get_bone_visibility(data_path):
bone, prop = split_path(data_path)
ob = bpy.context.object
b_layers = [i for i, val in enumerate(ob.pose.bones[bone].bone.layers) if val]
rig_layers = [(i, val) for i, val in enumerate(ob.data.layers)]
return ob.data.layers[b_layers[0]]
def get_keyframes(action, selected=False, includes=[]):
if selected:
# keyframes = sorted([int(k.co[0]) for f in action.fcurves for k in f.keyframe_points if k.select_control_point and get_bone_visibility(f.data_path)])
@ -60,19 +55,26 @@ def get_keyframes(action, selected=False, includes=[]):
continue
if not get_bone_visibility(f.data_path):
continue
keyframes += [int(k.co[0])]
keyframes += [int(k.co[0])]
if len(keyframes) <= 1:
keyframes = [bpy.context.scene.frame_current]
else:
keyframes = sorted([int(k.co[0]) for f in action.fcurves for k in f.keyframe_points])
keyframes = sorted(
[int(k.co[0]) for f in action.fcurves for k in f.keyframe_points]
)
return keyframes
def get_marker(action):
if action.pose_markers:
markers = action.pose_markers
return next((m.name for m in markers if m.frame == bpy.context.scene.frame_current), None)
return next(
(m.name for m in markers if m.frame == bpy.context.scene.frame_current),
None,
)
def reset_bone(bone, transform=True, custom_props=True):
if transform:
@ -83,41 +85,44 @@ def reset_bone(bone, transform=True, custom_props=True):
bone.rotation_axis_angle = (0, 0, 0, 0)
else:
bone.rotation_euler = (0, 0, 0)
bone.scale = (1, 1, 1)
if custom_props:
for key, value in bone.items():
try:
id_prop = bone.id_properties_ui(key)
except TypeError:
continue
if not isinstance(value, (int, float)) or not id_prop:
continue
bone[key] = id_prop.as_dict()['default']
bone[key] = id_prop.as_dict()["default"]
def is_asset_action(action):
return action.asset_data and action.asset_data.catalog_id != str(uuid.UUID(int=0))
def conform_action(action):
tags = ('pose', 'anim')
tags = ("pose", "anim")
if any(tag in action.asset_data.tags.keys() for tag in tags):
return
for fc in action.fcurves:
action.asset_data['is_single_frame'] = True
action.asset_data["is_single_frame"] = True
if len(fc.keyframe_points) > 1:
action.asset_data['is_single_frame'] = False
action.asset_data["is_single_frame"] = False
break
if action.asset_data['is_single_frame']:
action.asset_data.tags.new('pose')
if action.asset_data["is_single_frame"]:
action.asset_data.tags.new("pose")
else:
action.asset_data.tags.new('anim')
action.asset_data.tags.new("anim")
def clean_action(action='', frame_start=0, frame_end=0, excludes=[], includes=[]):
def clean_action(action="", frame_start=0, frame_end=0, excludes=[], includes=[]):
## Clean Keyframe Before/After Range
for fc in action.fcurves:
bone, prop = split_path(fc.data_path)
@ -133,19 +138,21 @@ def clean_action(action='', frame_start=0, frame_end=0, excludes=[], includes=[]
fc.update()
# Remove Keyframe out of range
for k in reversed(fc.keyframe_points):
if int(k.co[0]) not in range(frame_start, frame_end+1):
for k in reversed(fc.keyframe_points):
if int(k.co[0]) not in range(frame_start, frame_end + 1):
fc.keyframe_points.remove(k)
fc.update()
def append_action(action_path='', action_name=''):
print(f'Loading {action_name} from: {action_path}')
def append_action(action_path="", action_name=""):
print(f"Loading {action_name} from: {action_path}")
with bpy.data.libraries.load(str(action_path), link=False) as (data_from, data_to):
data_to.actions = [action_name]
return data_to.actions[0]
def apply_anim(action_lib, ob, bones=[]):
from mathutils import Vector
@ -153,23 +160,32 @@ def apply_anim(action_lib, ob, bones=[]):
if not ob.animation_data:
ob.animation_data_create()
action = ob.animation_data.action
if not action:
action = bpy.data.actions.new(ob.name)
ob.animation_data.action = action
keys = sorted([k.co[0] for f in action_lib.fcurves for k in f.keyframe_points])
if not keys:
print(f'The action {action_lib.name} has no keyframes')
print(f"The action {action_lib.name} has no keyframes")
return
first_key = keys[0]
key_offset = scn.frame_current - first_key
key_attr = ('type', 'interpolation', 'handle_left_type', 'handle_right_type',
'amplitude', 'back', 'easing', 'period', 'handle_right', 'handle_left'
key_attr = (
"type",
"interpolation",
"handle_left_type",
"handle_right_type",
"amplitude",
"back",
"easing",
"period",
"handle_right",
"handle_left",
)
for fc in action_lib.fcurves:
bone_name, prop_name = split_path(fc.data_path)
@ -182,26 +198,25 @@ def apply_anim(action_lib, ob, bones=[]):
action_fc = action.fcurves.new(
fc.data_path,
index=fc.array_index,
action_group=fc.group.name if fc.group else fc.data_path.split('"')[1]
action_group=fc.group.name if fc.group else fc.data_path.split('"')[1],
)
for kf_lib in fc.keyframe_points:
kf = action_fc.keyframe_points.insert(
frame=kf_lib.co[0] + key_offset,
value=kf_lib.co[1]
frame=kf_lib.co[0] + key_offset, value=kf_lib.co[1]
)
for attr in key_attr:
src_val = getattr(kf_lib, attr)
if attr.startswith('handle') and 'type' not in attr:
if attr.startswith("handle") and "type" not in attr:
src_val += Vector((key_offset, 0))
setattr(kf, attr, src_val)
fc.update()
# redraw graph areas
for window in bpy.context.window_manager.windows:
screen = window.screen
for area in screen.areas:
if area.type == 'GRAPH_EDITOR':
area.tag_redraw()
if area.type == "GRAPH_EDITOR":
area.tag_redraw()

View File

@ -1,4 +1,3 @@
import bpy
@ -6,44 +5,59 @@ def draw_context_menu(layout):
params = bpy.context.space_data.params
asset = bpy.context.asset_file_handle
layout.operator("assetlib.open_blend", text="Open blend file")#.asset = asset.name
layout.operator(
"assetlib.open_blend", text="Open blend file"
) # .asset = asset.name
layout.operator("assetlib.play_preview", text="Play Preview")
layout.separator()
layout.operator_context = 'INVOKE_DEFAULT'
layout.operator_context = "INVOKE_DEFAULT"
#layout.operator("assetlib.rename_asset", text="Rename Action")
# layout.operator("assetlib.rename_asset", text="Rename Action")
layout.operator("assetlib.remove_assets", text="Remove Assets")
layout.operator("assetlib.edit_data", text="Edit Asset data")
#layout.operator("actionlib.clear_asset", text="Clear Asset (Fake User)").use_fake_user = True
# layout.operator("actionlib.clear_asset", text="Clear Asset (Fake User)").use_fake_user = True
layout.separator()
layout.operator("actionlib.apply_selected_action", text="Apply Pose").flipped = False
layout.operator("actionlib.apply_selected_action", text="Apply Pose (Flipped)").flipped = True
layout.separator()
layout.operator("poselib.blend_pose_asset_for_keymap", text="Blend Pose").flipped = False
layout.operator("poselib.blend_pose_asset_for_keymap", text="Blend Pose (Flipped)").flipped = True
layout.operator("actionlib.apply_selected_action", text="Apply Pose").flipped = (
False
)
layout.operator(
"actionlib.apply_selected_action", text="Apply Pose (Flipped)"
).flipped = True
layout.separator()
layout.operator("poselib.pose_asset_select_bones", text="Select Bones").selected_side = 'CURRENT'
layout.operator("poselib.pose_asset_select_bones", text="Select Bones (Flipped)").selected_side = 'FLIPPED'
layout.operator("poselib.pose_asset_select_bones", text="Select Bones (Both)").selected_side = 'BOTH'
layout.operator(
"poselib.blend_pose_asset_for_keymap", text="Blend Pose"
).flipped = False
layout.operator(
"poselib.blend_pose_asset_for_keymap", text="Blend Pose (Flipped)"
).flipped = True
layout.separator()
#layout.operator("asset.library_refresh")
if params.display_type == 'THUMBNAIL':
layout.operator(
"poselib.pose_asset_select_bones", text="Select Bones"
).selected_side = "CURRENT"
layout.operator(
"poselib.pose_asset_select_bones", text="Select Bones (Flipped)"
).selected_side = "FLIPPED"
layout.operator(
"poselib.pose_asset_select_bones", text="Select Bones (Both)"
).selected_side = "BOTH"
layout.separator()
# layout.operator("asset.library_refresh")
if params.display_type == "THUMBNAIL":
layout.prop_menu_enum(params, "display_size")
def draw_header(layout):
'''Draw the header of the Asset Browser Window'''
"""Draw the header of the Asset Browser Window"""
layout.separator()
layout.operator("actionlib.store_anim_pose", text='Add Action', icon='FILE_NEW')
layout.operator("actionlib.store_anim_pose", text="Add Action", icon="FILE_NEW")

View File

@ -1,49 +1,63 @@
from typing import List, Tuple
import bpy
addon_keymaps: List[Tuple[bpy.types.KeyMap, bpy.types.KeyMapItem]] = []
def register():
wm = bpy.context.window_manager
addon = wm.keyconfigs.addon
if not addon:
return
km = addon.keymaps.new(name="File Browser Main", space_type="FILE_BROWSER")
# DblClick to apply pose.
kmi = km.keymap_items.new("actionlib.apply_selected_action", "LEFTMOUSE", "DOUBLE_CLICK")
kmi = km.keymap_items.new(
"actionlib.apply_selected_action", "LEFTMOUSE", "DOUBLE_CLICK"
)
kmi.properties.flipped = False
addon_keymaps.append((km, kmi))
kmi = km.keymap_items.new("actionlib.apply_selected_action", "LEFTMOUSE", "DOUBLE_CLICK", alt=True)
kmi = km.keymap_items.new(
"actionlib.apply_selected_action", "LEFTMOUSE", "DOUBLE_CLICK", alt=True
)
kmi.properties.flipped = True
addon_keymaps.append((km, kmi))
kmi = km.keymap_items.new("poselib.blend_pose_asset_for_keymap", "LEFTMOUSE", "DOUBLE_CLICK", shift=True)
kmi = km.keymap_items.new(
"poselib.blend_pose_asset_for_keymap", "LEFTMOUSE", "DOUBLE_CLICK", shift=True
)
kmi.properties.flipped = False
addon_keymaps.append((km, kmi))
kmi = km.keymap_items.new("poselib.blend_pose_asset_for_keymap", "LEFTMOUSE", "DOUBLE_CLICK", alt=True, shift=True)
kmi = km.keymap_items.new(
"poselib.blend_pose_asset_for_keymap",
"LEFTMOUSE",
"DOUBLE_CLICK",
alt=True,
shift=True,
)
kmi.properties.flipped = True
addon_keymaps.append((km, kmi))
kmi = km.keymap_items.new("poselib.pose_asset_select_bones", "S", "PRESS")
kmi.properties.selected_side = 'CURRENT'
kmi.properties.selected_side = "CURRENT"
addon_keymaps.append((km, kmi))
kmi = km.keymap_items.new("poselib.pose_asset_select_bones", "S", "PRESS", alt=True)
kmi.properties.selected_side = 'FLIPPED'
kmi.properties.selected_side = "FLIPPED"
addon_keymaps.append((km, kmi))
kmi = km.keymap_items.new("poselib.pose_asset_select_bones", "S", "PRESS", alt=True, ctrl=True)
kmi.properties.selected_side = 'BOTH'
kmi = km.keymap_items.new(
"poselib.pose_asset_select_bones", "S", "PRESS", alt=True, ctrl=True
)
kmi.properties.selected_side = "BOTH"
addon_keymaps.append((km, kmi))
def unregister():
for km, kmi in addon_keymaps:
km.keymap_items.remove(kmi)
addon_keymaps.clear()
addon_keymaps.clear()

File diff suppressed because it is too large Load Diff

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(
flipped: BoolProperty(
name="Flip Pose",
default=False,
)
previous_action : PointerProperty(type=bpy.types.Action)
publish_path : StringProperty(subtype='FILE_PATH')
camera : PointerProperty(type=bpy.types.Object, poll=lambda s, o: o.type == 'CAMERA')
rest_pose : PointerProperty(type=bpy.types.Action, poll=lambda s, a: a.asset_data)
previous_action: PointerProperty(type=bpy.types.Action)
publish_path: StringProperty(subtype="FILE_PATH")
camera: PointerProperty(type=bpy.types.Object, poll=lambda s, o: o.type == "CAMERA")
rest_pose: PointerProperty(type=bpy.types.Action, poll=lambda s, a: a.asset_data)
classes = (
ACTIONLIB_PG_scene,
)
classes = (ACTIONLIB_PG_scene,)
def register():
@ -24,6 +23,7 @@ def register():
bpy.types.Scene.actionlib = PointerProperty(type=ACTIONLIB_PG_scene)
def unregister():
try:
del bpy.types.Scene.actionlib
@ -31,4 +31,4 @@ def unregister():
pass
for cls in reversed(classes):
bpy.utils.unregister_class(cls)
bpy.utils.unregister_class(cls)

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]))
# sys.path.append(str(Path(__file__).parents[3]))
from asset_library.common.bl_utils import (
get_preview,
)
def rename_pose(src_name='', dst_name=''):
def rename_pose(src_name="", dst_name=""):
scn = bpy.context.scene
action = bpy.data.actions.get(src_name)
if not action:
print(f'No {src_name} not found.')
print(f"No {src_name} not found.")
bpy.ops.wm.quit_blender()
action.name = dst_name
@ -24,21 +25,21 @@ def rename_pose(src_name='', dst_name=''):
if preview:
preview.rename(re.sub(src_name, dst_name, str(preview)))
bpy.ops.wm.save_mainfile(
filepath=bpy.data.filepath, compress=True, exit=True
bpy.ops.wm.save_mainfile(filepath=bpy.data.filepath, compress=True, exit=True)
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Add Comment To the tracker",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
if __name__ == '__main__' :
parser = argparse.ArgumentParser(description='Add Comment To the tracker',
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument("--src-name")
parser.add_argument("--dst-name")
parser.add_argument('--src-name')
parser.add_argument('--dst-name')
if '--' in sys.argv :
index = sys.argv.index('--')
sys.argv = [sys.argv[index-1], *sys.argv[index+1:]]
if "--" in sys.argv:
index = sys.argv.index("--")
sys.argv = [sys.argv[index - 1], *sys.argv[index + 1 :]]
args = parser.parse_args()
rename_pose(**vars(args))

View File

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

View File

@ -1,27 +1,28 @@
from asset_library.collection import (
gui,
operators,
keymaps,
#build_collection_blends,
#create_collection_library,
)
# build_collection_blends,
# create_collection_library,
)
if 'bpy' in locals():
if "bpy" in locals():
import importlib
importlib.reload(gui)
importlib.reload(operators)
importlib.reload(keymaps)
#importlib.reload(build_collection_blends)
#importlib.reload(create_collection_library)
# importlib.reload(build_collection_blends)
# importlib.reload(create_collection_library)
import bpy
def register():
operators.register()
keymaps.register()
def unregister():
operators.unregister()
keymaps.unregister()
keymaps.unregister()

View File

@ -29,12 +29,13 @@ from asset_library.constants import ASSETLIB_FILENAME
]
"""
def build_collection_blends(path, categories=None, clean=True):
t0 = time()
scn = bpy.context.scene
scn.render.resolution_x = scn.render.resolution_y = 1000
json_path = Path(path) / ASSETLIB_FILENAME
if not json_path.exists():
return
@ -43,30 +44,33 @@ def build_collection_blends(path, categories=None, clean=True):
category_datas = json.loads(json_path.read_text())
for category_data in category_datas:
if categories and category_data['name'] not in categories:
if categories and category_data["name"] not in categories:
continue
bpy.ops.wm.read_homefile(use_empty=True)
# category_data = next(c for c in category_datas if c['name'] == category)
# _col_datas = category_data['children']
#category_data = next(c for c in category_datas if c['name'] == category)
#_col_datas = category_data['children']
cat_name = category_data['name']
build_path = Path(path) / cat_name / f'{cat_name}.blend'
cat_name = category_data["name"]
build_path = Path(path) / cat_name / f"{cat_name}.blend"
## re-iterate in grouped filepath
col_datas = sorted(category_data['children'], key=lambda x: x['filepath'])
for filepath, col_data_groups in groupby(col_datas, key=lambda x: x['filepath']):
#f = Path(f)
col_datas = sorted(category_data["children"], key=lambda x: x["filepath"])
for filepath, col_data_groups in groupby(
col_datas, key=lambda x: x["filepath"]
):
# f = Path(f)
if not Path(filepath).exists():
print(f'Not exists: {filepath}')
print(f"Not exists: {filepath}")
continue
col_data_groups = list(col_data_groups)
col_names = [a['name'] for a in col_data_groups]
linked_cols = load_datablocks(filepath, col_names, link=True, type='collections')
col_names = [a["name"] for a in col_data_groups]
linked_cols = load_datablocks(
filepath, col_names, link=True, type="collections"
)
for i, col in enumerate(linked_cols):
# iterate in linked collection and associated data
@ -78,14 +82,14 @@ def build_collection_blends(path, categories=None, clean=True):
## Directly link as collection inside a marked collection with same name
marked_col = col_as_asset(col, verbose=True)
marked_col.asset_data.description = asset_data.get('description', '')
marked_col.asset_data.catalog_id = category_data['id'] # assign catalog
marked_col.asset_data.description = asset_data.get("description", "")
marked_col.asset_data.catalog_id = category_data["id"] # assign catalog
for k, v in asset_data.get('metadata', {}).items():
for k, v in asset_data.get("metadata", {}).items():
marked_col.asset_data[k] = v
## exclude collections and generate preview
bpy.ops.ed.lib_id_generate_preview({"id": marked_col}) # preview gen
bpy.ops.ed.lib_id_generate_preview({"id": marked_col}) # preview gen
vcol = bpy.context.view_layer.layer_collection.children[marked_col.name]
vcol.exclude = True
@ -93,32 +97,36 @@ def build_collection_blends(path, categories=None, clean=True):
## clear all objects (can be very long with a lot of objects...):
if clean:
print('Removing links...')
print("Removing links...")
for lib in reversed(bpy.data.libraries):
bpy.data.libraries.remove(lib)
# Créer les dossiers intermediaires
build_path.parent.mkdir(parents=True, exist_ok=True)
print('Saving to', build_path)
print("Saving to", build_path)
bpy.ops.wm.save_as_mainfile(filepath=str(build_path), compress=False)
print("build time:", f'{time() - t0:.1f}s')
print("build time:", f"{time() - t0:.1f}s")
bpy.ops.wm.quit_blender()
if __name__ == '__main__' :
parser = argparse.ArgumentParser(description='build_collection_blends',
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="build_collection_blends",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
parser.add_argument('-path') # Trouve/créer le json assetlib.json en sous-dossier de libdir
parser.add_argument('--category') # Lit la category dans le json et a link tout dans le blend
parser.add_argument(
"-path"
) # Trouve/créer le json assetlib.json en sous-dossier de libdir
parser.add_argument(
"--category"
) # Lit la category dans le json et a link tout dans le blend
if '--' in sys.argv :
index = sys.argv.index('--')
sys.argv = [sys.argv[index-1], *sys.argv[index+1:]]
if "--" in sys.argv:
index = sys.argv.index("--")
sys.argv = [sys.argv[index - 1], *sys.argv[index + 1 :]]
args = parser.parse_args()
build_collection_blends(**vars(args))
build_collection_blends(**vars(args))

View File

@ -44,87 +44,93 @@ from asset_library.constants import ASSETLIB_FILENAME
]
"""
def create_collection_json(path, source_directory):
'''Create a Json from every marked collection in blends
"""Create a Json from every marked collection in blends
contained in folderpath (respect hierachy)
'''
"""
json_path = Path(path) / ASSETLIB_FILENAME
# scan all last version of the assets ?
# get last version files ?
# or open all blends and look only for marked collection ? (if versionned, get still get only last)
# get all blend in dir and subdirs (only last when versionned _v???)
blends = get_last_files(source_directory, pattern=r'(_v\d{3})?\.blend$', only_matching=True)
blends = get_last_files(
source_directory, pattern=r"(_v\d{3})?\.blend$", only_matching=True
)
root_path = Path(source_directory).as_posix().rstrip('/') + '/'
print('root_path: ', root_path)
root_path = Path(source_directory).as_posix().rstrip("/") + "/"
print("root_path: ", root_path)
# open and check data block marked as asset
category_datas = []
for i, blend in enumerate(blends):
fp = Path(blend)
print(f'{i+1}/{len(blends)}')
print(f"{i+1}/{len(blends)}")
## What is considered a grouping category ? top level folders ? parents[1] ?
## Remove root path and extension
## top level folder ('chars'), problem if blends at root
category = fp.as_posix().replace(root_path, '').split('/')[0]
## top level folder ('chars'), problem if blends at root
category = fp.as_posix().replace(root_path, "").split("/")[0]
## full blend path (chars/perso/perso)
# category = fp.as_posix().replace(root_path, '').rsplit('.', 1)[0]
print(category)
with bpy.data.libraries.load(blend, link=True, assets_only=True) as (data_from, data_to):
with bpy.data.libraries.load(blend, link=True, assets_only=True) as (
data_from,
data_to,
):
## just listing
col_name_list = [c for c in data_from.collections]
if not col_name_list:
continue
col_list = next((c['children'] for c in category_datas if c['name'] == category), None)
col_list = next(
(c["children"] for c in category_datas if c["name"] == category), None
)
if col_list is None:
col_list = []
category_data = {
'name': category,
'id': str(uuid.uuid4()),
'children': col_list,
}
"name": category,
"id": str(uuid.uuid4()),
"children": col_list,
}
category_datas.append(category_data)
blend_source_path = blend.as_posix()
if (project_root := os.environ.get('PROJECT_ROOT')):
blend_source_path = blend_source_path.replace(project_root, '$PROJECT_ROOT')
if project_root := os.environ.get("PROJECT_ROOT"):
blend_source_path = blend_source_path.replace(project_root, "$PROJECT_ROOT")
for name in col_name_list:
data = {
'filepath' : blend,
'name' : name,
"filepath": blend,
"name": name,
# 'tags' : [],
'metadata' : {'filepath': blend_source_path},
"metadata": {"filepath": blend_source_path},
}
col_list.append(data)
json_path.write_text(json.dumps(category_datas, indent='\t'))
json_path.write_text(json.dumps(category_datas, indent="\t"))
## create text catalog from json (keep_existing_category ?)
create_catalog_file(json_path, keep_existing_category=True)
def create_collection_library(path, source_directory=None):
'''
"""
path: store collection library (json and blends database)
source_directory: if a source is set, rebuild json and library
'''
"""
if source_directory:
if not Path(source_directory).exists():
print(f'Source directory not exists: {source_directory}')
print(f"Source directory not exists: {source_directory}")
return
## scan source and build json in assetlib dir root
@ -132,32 +138,45 @@ def create_collection_library(path, source_directory=None):
json_path = Path(path) / ASSETLIB_FILENAME
if not json_path.exists():
print(f'No json found at: {json_path}')
print(f"No json found at: {json_path}")
return
file_datas = json.loads(json_path.read())
## For each category in json, execute build_assets_blend script
script = Path(__file__).parent / 'build_collection_blends.py'
#empty_blend = Path(__file__).parent / 'empty_scene.blend'
script = Path(__file__).parent / "build_collection_blends.py"
# empty_blend = Path(__file__).parent / 'empty_scene.blend'
# for category, asset_datas in file_datas.items():
for category_data in file_datas:
## add an empty blend as second arg
cmd = [bpy.app.binary_path, '--python', str(script), '--', '--path', path, '--category', category_data['name']]
print('cmd: ', cmd)
cmd = [
bpy.app.binary_path,
"--python",
str(script),
"--",
"--path",
path,
"--category",
category_data["name"],
]
print("cmd: ", cmd)
subprocess.call(cmd)
if __name__ == '__main__' :
parser = argparse.ArgumentParser(description='Create Collection Library',
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Create Collection Library",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
parser.add_argument('--path') # trouve/créer le json assetlib.json en sous-dossier de libdir
parser.add_argument(
"--path"
) # trouve/créer le json assetlib.json en sous-dossier de libdir
if '--' in sys.argv :
index = sys.argv.index('--')
sys.argv = [sys.argv[index-1], *sys.argv[index+1:]]
if "--" in sys.argv:
index = sys.argv.index("--")
sys.argv = [sys.argv[index - 1], *sys.argv[index + 1 :]]
args = parser.parse_args()
create_collection_library(**vars(args))
create_collection_library(**vars(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
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,10 +12,13 @@ def register():
return
km = addon.keymaps.new(name="File Browser Main", space_type="FILE_BROWSER")
kmi = km.keymap_items.new("assetlib.load_asset", "LEFTMOUSE", "DOUBLE_CLICK") # , shift=True
kmi = km.keymap_items.new(
"assetlib.load_asset", "LEFTMOUSE", "DOUBLE_CLICK"
) # , shift=True
addon_keymaps.append((km, kmi))
def unregister():
for km, kmi in addon_keymaps:
km.keymap_items.remove(kmi)
addon_keymaps.clear()
addon_keymaps.clear()

View File

@ -14,24 +14,23 @@ from asset_library.common.bl_utils import load_col
from asset_library.common.functions import get_active_library
class ASSETLIB_OT_load_asset(Operator):
bl_idname = "assetlib.load_asset"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
bl_label = 'Load Asset'
bl_description = 'Link and override asset in current file'
bl_label = "Load Asset"
bl_description = "Link and override asset in current file"
@classmethod
def poll(cls, context: Context) -> bool:
if not asset_utils.SpaceAssetInfo.is_asset_browser(context.space_data):
cls.poll_message_set("Current editor is not an asset browser")
return False
lib = get_active_library()
if not lib or lib.data_type != 'COLLECTION':
if not lib or lib.data_type != "COLLECTION":
return False
if not context.active_file or 'filepath' not in context.active_file.asset_data:
if not context.active_file or "filepath" not in context.active_file.asset_data:
cls.poll_message_set("Has not filepath property")
return False
@ -39,51 +38,49 @@ class ASSETLIB_OT_load_asset(Operator):
def execute(self, context: Context) -> Set[str]:
print('Load Asset')
print("Load Asset")
lib = get_active_library()
asset = context.active_file
if not asset:
self.report({"ERROR"}, 'No asset selected')
return {'CANCELLED'}
self.report({"ERROR"}, "No asset selected")
return {"CANCELLED"}
active_lib = lib.library_type.get_active_asset_library()
asset_path = asset.asset_data['filepath']
asset_path = asset.asset_data["filepath"]
asset_path = active_lib.library_type.format_path(asset_path)
name = asset.name
## set mode to object
if context.mode != 'OBJECT':
bpy.ops.object.mode_set(mode='OBJECT')
if context.mode != "OBJECT":
bpy.ops.object.mode_set(mode="OBJECT")
if not Path(asset_path).exists():
self.report({'ERROR'}, f'Not exists: {asset_path}')
return {'CANCELLED'}
self.report({"ERROR"}, f"Not exists: {asset_path}")
return {"CANCELLED"}
print('Load collection', asset_path, name)
res = load_col(asset_path, name, link=True, override=True, rig_pattern='*_rig')
print("Load collection", asset_path, name)
res = load_col(asset_path, name, link=True, override=True, rig_pattern="*_rig")
if res:
if res.type == 'ARMATURE':
self.report({'INFO'}, f'Override rig {res.name}')
elif res.type == 'EMPTY':
self.report({'INFO'}, f'Instance collection {res.name}')
if res.type == "ARMATURE":
self.report({"INFO"}, f"Override rig {res.name}")
elif res.type == "EMPTY":
self.report({"INFO"}, f"Instance collection {res.name}")
return {'FINISHED'}
return {"FINISHED"}
### --- REGISTER ---
classes = (
ASSETLIB_OT_load_asset,
)
classes = (ASSETLIB_OT_load_asset,)
def register():
for cls in classes:
bpy.utils.register_class(cls)
def unregister():
for cls in reversed(classes):
bpy.utils.unregister_class(cls)
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)
@ -15,4 +13,4 @@ if 'bpy' in locals():
importlib.reload(template)
importlib.reload(catalog)
import bpy
import bpy

View File

@ -1,4 +1,3 @@
"""
Generic Blender functions
"""
@ -6,29 +5,31 @@ Generic Blender functions
from pathlib import Path
from fnmatch import fnmatch
from typing import Any, List, Iterable, Optional, Tuple
Datablock = Any
import bpy
from bpy_extras import asset_utils
from asset_library.constants import RESOURCES_DIR
#from asset_library.common.file_utils import no
# from asset_library.common.file_utils import no
from os.path import abspath
import subprocess
class attr_set():
'''Receive a list of tuple [(data_path, "attribute" [, wanted value)] ]
class attr_set:
"""Receive a list of tuple [(data_path, "attribute" [, wanted value)] ]
entering with-statement : Store existing values, assign wanted value (if any)
exiting with-statement: Restore values to their old values
'''
"""
def __init__(self, attrib_list):
self.store = []
# item = (prop, attr, [new_val])
for item in attrib_list:
prop, attr = item[:2]
self.store.append( (prop, attr, getattr(prop, attr)) )
self.store.append((prop, attr, getattr(prop, attr)))
for item in attrib_list:
prop, attr = item[:2]
@ -36,7 +37,7 @@ class attr_set():
try:
setattr(prop, attr, item[2])
except TypeError:
print(f'Cannot set attribute {attr} to {prop}')
print(f"Cannot set attribute {attr} to {prop}")
def __enter__(self):
return self
@ -48,25 +49,38 @@ class attr_set():
for prop, attr, old_val in self.store:
setattr(prop, attr, old_val)
def get_overriden_col(ob, scene=None):
scn = scene or bpy.context.scene
cols = [c for c in bpy.data.collections if scn.user_of_id(c)]
return next((c for c in cols if ob in c.all_objects[:]
if all(not c.override_library for c in get_col_parents(c))), None)
return next(
(
c
for c in cols
if ob in c.all_objects[:]
if all(not c.override_library for c in get_col_parents(c))
),
None,
)
def get_view3d_persp():
windows = bpy.context.window_manager.windows
view_3ds = [a for w in windows for a in w.screen.areas if a.type == 'VIEW_3D']
view_3d = next((a for a in view_3ds if a.spaces.active.region_3d.view_perspective == 'PERSP'), view_3ds[0])
view_3ds = [a for w in windows for a in w.screen.areas if a.type == "VIEW_3D"]
view_3d = next(
(a for a in view_3ds if a.spaces.active.region_3d.view_perspective == "PERSP"),
view_3ds[0],
)
return view_3d
def get_viewport():
screen = bpy.context.screen
areas = [a for a in screen.areas if a.type == 'VIEW_3D']
areas.sort(key=lambda x : x.width*x.height)
areas = [a for a in screen.areas if a.type == "VIEW_3D"]
areas.sort(key=lambda x: x.width * x.height)
return areas[-1]
@ -81,7 +95,7 @@ def biggest_asset_browser_area(screen: bpy.types.Screen) -> Optional[bpy.types.A
def area_sorting_key(area: bpy.types.Area) -> Tuple[bool, int]:
"""Return area size in pixels."""
return (area.width * area.height)
return area.width * area.height
areas = list(suitable_areas(screen))
if not areas:
@ -89,6 +103,7 @@ def biggest_asset_browser_area(screen: bpy.types.Screen) -> Optional[bpy.types.A
return max(areas, key=area_sorting_key)
def suitable_areas(screen: bpy.types.Screen) -> Iterable[bpy.types.Area]:
"""Generator, yield Asset Browser areas."""
@ -98,6 +113,7 @@ def suitable_areas(screen: bpy.types.Screen) -> Iterable[bpy.types.Area]:
continue
yield area
def area_from_context(context: bpy.types.Context) -> Optional[bpy.types.Area]:
"""Return an Asset Browser suitable for the given category.
@ -122,6 +138,7 @@ def area_from_context(context: bpy.types.Context) -> Optional[bpy.types.Area]:
return None
def activate_asset(
asset: bpy.types.Action, asset_browser: bpy.types.Area, *, deferred: bool
) -> None:
@ -131,19 +148,25 @@ def activate_asset(
assert asset_utils.SpaceAssetInfo.is_asset_browser(space_data)
space_data.activate_asset_by_id(asset, deferred=deferred)
def active_catalog_id(asset_browser: bpy.types.Area) -> str:
"""Return the ID of the catalog shown in the asset browser."""
return params(asset_browser).catalog_id
def get_asset_space_params(asset_browser: bpy.types.Area) -> bpy.types.FileAssetSelectParams:
def get_asset_space_params(
asset_browser: bpy.types.Area,
) -> bpy.types.FileAssetSelectParams:
"""Return the asset browser parameters given its Area."""
space_data = asset_browser.spaces[0]
assert asset_utils.SpaceAssetInfo.is_asset_browser(space_data)
return space_data.params
def refresh_asset_browsers():
for area in suitable_areas(bpy.context.screen):
bpy.ops.asset.library_refresh({"area": area, 'region': area.regions[3]})
bpy.ops.asset.library_refresh({"area": area, "region": area.regions[3]})
def tag_redraw(screen: bpy.types.Screen) -> None:
"""Tag all asset browsers for redrawing."""
@ -151,6 +174,7 @@ def tag_redraw(screen: bpy.types.Screen) -> None:
for area in suitable_areas(screen):
area.tag_redraw()
# def get_blender_command(file=None, script=None, background=True, **args):
# '''Return a Blender Command as a list to be used in a subprocess'''
@ -166,9 +190,10 @@ def tag_redraw(screen: bpy.types.Screen) -> None:
# cmd += ['--']
# for k, v in args.items():
# cmd += [f"--{k.replace('_', '-')}", str(v)]
# return cmd
def norm_value(value):
if isinstance(value, (tuple, list)):
values = []
@ -177,7 +202,7 @@ def norm_value(value):
v = json.dumps(v)
values.append(v)
return values
return values
if isinstance(value, Path):
return str(value)
@ -186,31 +211,35 @@ def norm_value(value):
value = json.dumps(value)
return value
def norm_arg(arg_name, format=str.lower, prefix='--', separator='-'):
def norm_arg(arg_name, format=str.lower, prefix="--", separator="-"):
arg_name = norm_str(arg_name, format=format, separator=separator)
return prefix + arg_name
def get_bl_cmd(blender=None, background=False, focus=True, blendfile=None, script=None, **kargs):
def get_bl_cmd(
blender=None, background=False, focus=True, blendfile=None, script=None, **kargs
):
cmd = [str(blender)] if blender else [bpy.app.binary_path]
if background:
cmd += ['--background']
cmd += ["--background"]
if not focus and not background:
cmd += ['--no-window-focus']
cmd += ['--window-geometry', '5000', '0', '10', '10']
cmd += ["--no-window-focus"]
cmd += ["--window-geometry", "5000", "0", "10", "10"]
cmd += ['--python-use-system-env']
cmd += ["--python-use-system-env"]
if blendfile:
cmd += [str(blendfile)]
if script:
cmd += ['--python', str(script)]
cmd += ["--python", str(script)]
if kargs:
cmd += ['--']
cmd += ["--"]
for k, v in kargs.items():
k = norm_arg(k)
v = norm_value(v)
@ -223,18 +252,18 @@ def get_bl_cmd(blender=None, background=False, focus=True, blendfile=None, scrip
return cmd
def get_addon_prefs():
addon_name = __package__.split('.')[0]
return bpy.context.preferences.addons[addon_name].preferences
def get_addon_prefs():
addon_name = __package__.split(".")[0]
return bpy.context.preferences.addons[addon_name].preferences
def thumbnail_blend_file(input_blend, output_img):
input_blend = Path(input_blend).resolve()
output_img = Path(output_img).resolve()
print(f'Thumbnailing {input_blend} to {output_img}')
blender_thumbnailer = Path(bpy.app.binary_path).parent / 'blender-thumbnailer'
print(f"Thumbnailing {input_blend} to {output_img}")
blender_thumbnailer = Path(bpy.app.binary_path).parent / "blender-thumbnailer"
output_img.parent.mkdir(exist_ok=True, parents=True)
@ -243,16 +272,17 @@ def thumbnail_blend_file(input_blend, output_img):
success = output_img.exists()
if not success:
empty_preview = RESOURCES_DIR / 'empty_preview.png'
empty_preview = RESOURCES_DIR / "empty_preview.png"
shutil.copy(str(empty_preview), str(output_img))
return success
def get_col_parents(col, root=None, cols=None):
'''Return a list of parents collections of passed col
"""Return a list of parents collections of passed col
root : Pass a collection to search in (recursive)
else search in master collection
'''
"""
if cols is None:
cols = []
@ -267,14 +297,23 @@ def get_col_parents(col, root=None, cols=None):
cols = get_col_parents(col, root=sub, cols=cols)
return cols
def get_overriden_col(ob, scene=None):
'''Get the collection use for making the override'''
"""Get the collection use for making the override"""
scn = scene or bpy.context.scene
cols = [c for c in bpy.data.collections if scn.user_of_id(c)]
return next((c for c in cols if ob in c.all_objects[:]
if all(not c.override_library for c in get_col_parents(c))), None)
return next(
(
c
for c in cols
if ob in c.all_objects[:]
if all(not c.override_library for c in get_col_parents(c))
),
None,
)
def load_assets_from(filepath: Path) -> List[Datablock]:
if not has_assets(filepath):
@ -306,6 +345,7 @@ def load_assets_from(filepath: Path) -> List[Datablock]:
loaded_assets.append(datablock)
return loaded_assets
def has_assets(filepath: Path) -> bool:
with bpy.data.libraries.load(str(filepath), assets_only=True) as (
data_from,
@ -318,51 +358,49 @@ def has_assets(filepath: Path) -> bool:
return False
def copy_frames(start, end, offset, path):
for i in range (start, end):
src = path.replace('####', f'{i:04d}')
dst = src.replace(src.split('_')[-1].split('.')[0], f'{i+offset:04d}')
for i in range(start, end):
src = path.replace("####", f"{i:04d}")
dst = src.replace(src.split("_")[-1].split(".")[0], f"{i+offset:04d}")
shutil.copy2(src, dst)
def split_path(path) :
try :
def split_path(path):
try:
bone_name = path.split('["')[1].split('"]')[0]
except :
except:
bone_name = None
try :
try:
prop_name = path.split('["')[2].split('"]')[0]
except :
prop_name = path.split('.')[-1]
except:
prop_name = path.split(".")[-1]
return bone_name, prop_name
def load_datablocks(src, names=None, type='objects', link=True, expr=None, assets_only=False) -> list:
def load_datablocks(
src, names=None, type="objects", link=True, expr=None, assets_only=False
) -> list:
return_list = not isinstance(names, str)
names = names or []
if not isinstance(names, (list, tuple)):
names = [names]
if isinstance(expr, str):
pattern = expr
expr = lambda x : fnmatch(x, pattern)
with bpy.data.libraries.load(str(src), link=link,assets_only=assets_only) as (data_from, data_to):
expr = lambda x: fnmatch(x, pattern)
with bpy.data.libraries.load(str(src), link=link, assets_only=assets_only) as (
data_from,
data_to,
):
datablocks = getattr(data_from, type)
if expr:
names += [i for i in datablocks if expr(i)]
elif not names:
names = datablocks
setattr(data_to, type, names)
datablocks = getattr(data_to, type)
@ -373,23 +411,26 @@ def load_datablocks(src, names=None, type='objects', link=True, expr=None, asset
elif datablocks:
return datablocks[0]
"""
# --- Collection handling
"""
def col_as_asset(col, verbose=False):
if col is None:
return
if verbose:
print('linking:', col.name)
print("linking:", col.name)
pcol = bpy.data.collections.new(col.name)
bpy.context.scene.collection.children.link(pcol)
pcol.children.link(col)
pcol.asset_mark()
return pcol
def load_col(filepath, name, link=True, override=True, rig_pattern=None, context=None):
'''Link a collection by name from a file and override if has armature'''
"""Link a collection by name from a file and override if has armature"""
# with bpy.data.libraries.load(filepath, link=link) as (data_from, data_to):
# data_to.collections = [c for c in data_from.collections if c == name]
@ -398,12 +439,12 @@ def load_col(filepath, name, link=True, override=True, rig_pattern=None, context
# return data_to.collections[0]
context = context or bpy.context
col = load_datablocks(filepath, name, link=link, type='collections')
col = load_datablocks(filepath, name, link=link, type="collections")
## create instance object
inst = bpy.data.objects.new(col.name, None)
inst.instance_collection = col
inst.instance_type = 'COLLECTION'
inst.instance_type = "COLLECTION"
context.scene.collection.objects.link(inst)
# make active
@ -413,26 +454,29 @@ def load_col(filepath, name, link=True, override=True, rig_pattern=None, context
## simple object (no armatures)
if not link or not override:
return inst
if not next((o for o in col.all_objects if o.type == 'ARMATURE'), None):
if not next((o for o in col.all_objects if o.type == "ARMATURE"), None):
return inst
## Create the override
# Search
parent_cols = inst.users_collection
child_cols = [child for pcol in parent_cols for child in pcol.children]
params = {'active_object': inst, 'selected_objects': [inst]}
params = {"active_object": inst, "selected_objects": [inst]}
try:
bpy.ops.object.make_override_library(params)
## check which collection is new in parents collection
asset_col = next((c for pcol in parent_cols for c in pcol.children if c not in child_cols), None)
asset_col = next(
(c for pcol in parent_cols for c in pcol.children if c not in child_cols),
None,
)
if not asset_col:
print('Overriden, but no collection found !!')
print("Overriden, but no collection found !!")
return
for ob in asset_col.all_objects:
if ob.type != 'ARMATURE':
if ob.type != "ARMATURE":
continue
if rig_pattern and not fnmatch(ob.name, rig_pattern):
continue
@ -444,37 +488,40 @@ def load_col(filepath, name, link=True, override=True, rig_pattern=None, context
return ob
except Exception as e:
print(f'Override failed on {col.name}')
print(f"Override failed on {col.name}")
print(e)
return inst
def get_preview(asset_path='', asset_name=''):
def get_preview(asset_path="", asset_name=""):
asset_preview_dir = Path(asset_path).parents[1]
name = asset_name.lower()
return next((f for f in asset_preview_dir.rglob('*') if f.stem.lower().endswith(name)), None)
return next(
(f for f in asset_preview_dir.rglob("*") if f.stem.lower().endswith(name)), None
)
def get_object_libraries(ob):
if ob is None:
return []
libraries = [ob.library]
if ob.data:
libraries += [ob.data.library]
if ob.type in ('MESH', 'CURVE'):
if ob.type in ("MESH", "CURVE"):
libraries += [m.library for m in ob.data.materials if m]
filepaths = []
for l in libraries:
if not l or not l.filepath:
continue
absolute_filepath = abspath(bpy.path.abspath(l.filepath, library=l))
if absolute_filepath in filepaths:
continue
filepaths.append(absolute_filepath)
return filepaths
return 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
@ -16,7 +16,7 @@ class CatalogItem:
if isinstance(self.path, Path):
self.path = self.path.as_posix()
if self.path and not self.name:
self.name = self.norm_name(self.path)
@ -25,10 +25,10 @@ class CatalogItem:
def norm_name(self, name):
"""Get a norm name from a catalog_path entry"""
return name.replace('/', '-')
return name.replace("/", "-")
def __repr__(self):
return f'CatalogItem(name={self.name}, path={self.path}, id={self.id})'
return f"CatalogItem(name={self.name}, path={self.path}, id={self.id})"
class CatalogContext:
@ -60,11 +60,12 @@ class CatalogContext:
if self.active_item:
return self.active_item.path
return ''
return ""
class Catalog:
"""Represent the catalog of the blender asset browser library"""
def __init__(self, directory=None):
self.directory = None
@ -72,14 +73,14 @@ class Catalog:
if directory:
self.directory = Path(directory)
self.context = CatalogContext()
@property
def filepath(self):
"""Get the filepath of the catalog text file relative to the directory"""
if self.directory:
return self.directory /'blender_assets.cats.txt'
return self.directory / "blender_assets.cats.txt"
def read(self):
"""Read the catalog file of the library target directory or of the specified directory"""
@ -88,47 +89,49 @@ class Catalog:
return {}
self._data.clear()
print(f'Read catalog from {self.filepath}')
for line in self.filepath.read_text(encoding="utf-8").split('\n'):
if line.startswith(('VERSION', '#')) or not line:
print(f"Read catalog from {self.filepath}")
for line in self.filepath.read_text(encoding="utf-8").split("\n"):
if line.startswith(("VERSION", "#")) or not line:
continue
cat_id, cat_path, cat_name = line.split(':')
self._data[cat_id] = CatalogItem(self, name=cat_name, id=cat_id, path=cat_path)
cat_id, cat_path, cat_name = line.split(":")
self._data[cat_id] = CatalogItem(
self, name=cat_name, id=cat_id, path=cat_path
)
return self
def write(self, sort=True):
"""Write the catalog file in the library target directory or of the specified directory"""
if not self.filepath:
raise Exception(f'Cannot write catalog {self} no filepath setted')
lines = ['VERSION 1', '']
if not self.filepath:
raise Exception(f"Cannot write catalog {self} no filepath setted")
lines = ["VERSION 1", ""]
catalog_items = list(self)
if sort:
catalog_items.sort(key=lambda x : x.path)
catalog_items.sort(key=lambda x: x.path)
for catalog_item in catalog_items:
lines.append(f"{catalog_item.id}:{catalog_item.path}:{catalog_item.name}")
print(f'Write Catalog at: {self.filepath}')
self.filepath.write_text('\n'.join(lines), encoding="utf-8")
print(f"Write Catalog at: {self.filepath}")
self.filepath.write_text("\n".join(lines), encoding="utf-8")
def get(self, path=None, id=None, fallback=None):
"""Found a catalog item by is path or id"""
if isinstance(path, Path):
path = path.as_posix()
if id:
return self._data.get(id)
for catalog_item in self:
if catalog_item.path == path:
return catalog_item
return fallback
def remove(self, catalog_item):
@ -140,7 +143,7 @@ class Catalog:
if catalog_item:
return self._data.pop(catalog_item.id)
print(f'Warning: {catalog_item} cannot be remove, not in {self}')
print(f"Warning: {catalog_item} cannot be remove, not in {self}")
return None
def add(self, catalog_path):
@ -151,7 +154,7 @@ class Catalog:
print(parent, self.get(parent))
if self.get(parent):
continue
cat_item = CatalogItem(self, path=parent)
self._data[cat_item.id] = cat_item
@ -161,19 +164,23 @@ class Catalog:
self._data[cat_item.id] = cat_item
return cat_item
def update(self, catalogs):
'Add or remove catalog entries if on the list given or not'
catalogs = set(catalogs) # Remove doubles
def update(self, catalogs):
"Add or remove catalog entries if on the list given or not"
catalogs = set(catalogs) # Remove doubles
added = [c for c in catalogs if not self.get(path=c)]
removed = [c.path for c in self if c.path not in catalogs]
if added:
print(f'{len(added)} Catalog Entry Added \n{tuple(c.name for c in added[:10])}...\n')
print(
f"{len(added)} Catalog Entry Added \n{tuple(c.name for c in added[:10])}...\n"
)
if removed:
print(f'{len(removed)} Catalog Entry Removed \n{tuple(c.name for c in removed[:10])}...\n')
print(
f"{len(removed)} Catalog Entry Removed \n{tuple(c.name for c in removed[:10])}...\n"
)
for catalog_item in removed:
self.remove(catalog_item)
@ -183,7 +190,7 @@ class Catalog:
def __iter__(self):
return self._data.values().__iter__()
def __getitem__(self, key):
if isinstance(key, int):
return self._data.values()[key]
@ -191,10 +198,10 @@ class Catalog:
return self._data[key]
def __contains__(self, item):
if isinstance(item, str): # item is the id
if isinstance(item, str): # item is the id
return item in self._data
else:
return item in self
def __repr__(self):
return f'Catalog(filepath={self.filepath})'
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
@ -46,50 +50,64 @@ def import_module_from_path(path):
path = Path(path)
spec = util.spec_from_file_location(path.stem, str(path))
mod = util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod
except Exception as e:
print(f'Cannot import file {path}')
print(f"Cannot import file {path}")
print(e)
def norm_str(string, separator='_', format=str.lower, padding=0):
def norm_str(string, separator="_", format=str.lower, padding=0):
string = str(string)
string = string.replace('_', ' ')
string = string.replace('-', ' ')
string = re.sub('[ ]+', ' ', string)
string = re.sub('[ ]+\/[ ]+', '/', string)
string = string.replace("_", " ")
string = string.replace("-", " ")
string = re.sub("[ ]+", " ", string)
string = re.sub("[ ]+\/[ ]+", "/", string)
string = string.strip()
if format:
string = format(string)
# Padd rightest number
string = re.sub(r'(\d+)(?!.*\d)', lambda x : x.group(1).zfill(padding), string)
string = string.replace(' ', separator)
string = unicodedata.normalize('NFKD', string).encode('ASCII', 'ignore').decode("utf-8")
string = re.sub(r"(\d+)(?!.*\d)", lambda x: x.group(1).zfill(padding), string)
string = string.replace(" ", separator)
string = (
unicodedata.normalize("NFKD", string).encode("ASCII", "ignore").decode("utf-8")
)
return string
def remove_version(filepath):
pattern = '_v[0-9]+\.'
pattern = "_v[0-9]+\."
search = re.search(pattern, filepath)
if search:
filepath = filepath.replace(search.group()[:-1], '')
filepath = filepath.replace(search.group()[:-1], "")
return Path(filepath).name
def is_exclude(name, patterns) -> bool:
# from fnmatch import fnmatch
if not isinstance(patterns, (list,tuple)) :
if not isinstance(patterns, (list, tuple)):
patterns = [patterns]
return any([fnmatch(name, p) for p in patterns])
def get_last_files(root, pattern=r'_v\d{3}\.\w+', only_matching=False, ex_file=None, ex_dir=None, keep=1, verbose=False) -> list:
'''Recursively get last(s) file(s) (when there is multiple versions) in passed directory
def get_last_files(
root,
pattern=r"_v\d{3}\.\w+",
only_matching=False,
ex_file=None,
ex_dir=None,
keep=1,
verbose=False,
) -> list:
"""Recursively get last(s) file(s) (when there is multiple versions) in passed directory
root -> str: Filepath of the folder to scan.
pattern -> str: Regex pattern to group files.
only_matching -> bool: Discard files that aren't matched by regex pattern.
@ -97,7 +115,7 @@ def get_last_files(root, pattern=r'_v\d{3}\.\w+', only_matching=False, ex_file=N
ex_dir -> list : List of fn_match pattern of directory name to skip.
keep -> int: Number of lasts versions to keep when there are mutliple versionned files (e.g: 1 keep only last).
verbose -> bool: Print infos in console.
'''
"""
files = []
if ex_file is None:
@ -111,7 +129,9 @@ def get_last_files(root, pattern=r'_v\d{3}\.\w+', only_matching=False, ex_file=N
dirs = [f for f in all_items if f.is_dir()]
for i in range(len(allfiles)-1,-1,-1):# fastest way to iterate on index in reverse
for i in range(
len(allfiles) - 1, -1, -1
): # fastest way to iterate on index in reverse
if not re.search(pattern, allfiles[i].name):
if only_matching:
allfiles.pop(i)
@ -119,7 +139,10 @@ def get_last_files(root, pattern=r'_v\d{3}\.\w+', only_matching=False, ex_file=N
files.append(allfiles.pop(i).path)
# separate remaining files in prefix grouped lists
lilist = [list(v) for k, v in groupby(allfiles, key=lambda x: re.split(pattern, x.name)[0])]
lilist = [
list(v)
for k, v in groupby(allfiles, key=lambda x: re.split(pattern, x.name)[0])
]
# get only item last of each sorted grouplist
for l in lilist:
@ -128,17 +151,26 @@ def get_last_files(root, pattern=r'_v\d{3}\.\w+', only_matching=False, ex_file=N
files.append(f.path)
if verbose and len(l) > 1:
print(f'{root}: keep {str([x.name for x in versions])} out of {len(l)} elements')
print(
f"{root}: keep {str([x.name for x in versions])} out of {len(l)} elements"
)
for d in dirs: # recursively treat all detected directory
for d in dirs: # recursively treat all detected directory
if ex_dir and is_exclude(d.name, ex_dir):
# skip folder with excluded name
# skip folder with excluded name
continue
files += get_last_files(
d.path, pattern=pattern, only_matching=only_matching, ex_file=ex_file, ex_dir=ex_dir, keep=keep)
d.path,
pattern=pattern,
only_matching=only_matching,
ex_file=ex_file,
ex_dir=ex_dir,
keep=keep,
)
return sorted(files)
def copy_file(src, dst, only_new=False, only_recent=False):
if dst.exists():
if only_new:
@ -147,19 +179,20 @@ def copy_file(src, dst, only_new=False, only_recent=False):
return
dst.parent.mkdir(exist_ok=True, parents=True)
print(f'Copy file from {src} to {dst}')
if platform.system() == 'Windows':
subprocess.call(['copy', str(src), str(dst)], shell=True)
print(f"Copy file from {src} to {dst}")
if platform.system() == "Windows":
subprocess.call(["copy", str(src), str(dst)], shell=True)
else:
subprocess.call(['cp', str(src), str(dst)])
subprocess.call(["cp", str(src), str(dst)])
def copy_dir(src, dst, only_new=False, only_recent=False, excludes=['.*'], includes=[]):
def copy_dir(src, dst, only_new=False, only_recent=False, excludes=[".*"], includes=[]):
src, dst = Path(src), Path(dst)
if includes:
includes = r'|'.join([fnmatch.translate(x) for x in includes])
includes = r"|".join([fnmatch.translate(x) for x in includes])
if excludes:
excludes = r'|'.join([fnmatch.translate(x) for x in excludes])
excludes = r"|".join([fnmatch.translate(x) for x in excludes])
if dst.is_dir():
dst.mkdir(exist_ok=True, parents=True)
@ -170,149 +203,157 @@ def copy_dir(src, dst, only_new=False, only_recent=False, excludes=['.*'], inclu
copy_file(src, dst, only_new=only_new, only_recent=only_recent)
elif src.is_dir():
src_files = list(src.rglob('*'))
src_files = list(src.rglob("*"))
if excludes:
src_files = [f for f in src_files if not re.match(excludes, f.name)]
if includes:
src_files = [f for f in src_files if re.match(includes, f.name)]
dst_files = [dst/f.relative_to(src) for f in src_files]
dst_files = [dst / f.relative_to(src) for f in src_files]
for src_file, dst_file in zip(src_files, dst_files) :
for src_file, dst_file in zip(src_files, dst_files):
if src_file.is_dir():
dst_file.mkdir(exist_ok=True, parents=True)
else:
copy_file(src_file, dst_file, only_new=only_new, only_recent=only_recent)
copy_file(
src_file, dst_file, only_new=only_new, only_recent=only_recent
)
def open_file(filepath, select=False):
'''Open a filepath inside the os explorer'''
if platform.system() == 'Darwin': # macOS
cmd = ['open']
elif platform.system() == 'Windows': # Windows
cmd = ['explorer']
"""Open a filepath inside the os explorer"""
if platform.system() == "Darwin": # macOS
cmd = ["open"]
elif platform.system() == "Windows": # Windows
cmd = ["explorer"]
if select:
cmd += ['/select,']
else: # linux variants
cmd = ['xdg-open']
cmd += ["/select,"]
else: # linux variants
cmd = ["xdg-open"]
if select:
cmd = ['nemo']
cmd = ["nemo"]
cmd += [str(filepath)]
subprocess.Popen(cmd)
def open_blender_file(filepath=None):
filepath = filepath or bpy.data.filepath
cmd = sys.argv
# if no filepath, use command as is to reopen blender
if filepath != '':
if len(cmd) > 1 and cmd[1].endswith('.blend'):
if filepath != "":
if len(cmd) > 1 and cmd[1].endswith(".blend"):
cmd[1] = str(filepath)
else:
cmd.insert(1, str(filepath))
subprocess.Popen(cmd)
def read_file(path):
'''Read a file with an extension in (json, yaml, yml, txt)'''
exts = ('.json', '.yaml', '.yml', '.txt')
def read_file(path):
"""Read a file with an extension in (json, yaml, yml, txt)"""
exts = (".json", ".yaml", ".yml", ".txt")
if not path:
print('Try to read empty file')
print("Try to read empty file")
path = Path(path)
if not path.exists():
print('File not exist', path)
print("File not exist", path)
return
if path.suffix not in exts:
print(f'Cannot read file {path}, extension must be in {exts}')
print(f"Cannot read file {path}, extension must be in {exts}")
return
txt = path.read_text()
data = None
if path.suffix.lower() in ('.yaml', '.yml'):
yaml = install_module('yaml')
if path.suffix.lower() in (".yaml", ".yml"):
yaml = install_module("yaml")
try:
data = yaml.safe_load(txt)
except Exception:
print(f'Could not load yaml file {path}')
print(f"Could not load yaml file {path}")
return
elif path.suffix.lower() == '.json':
elif path.suffix.lower() == ".json":
try:
data = json.loads(txt)
except Exception:
print(f'Could not load json file {path}')
print(f"Could not load json file {path}")
return
else:
data = txt
return data
def write_file(path, data, indent=4):
'''Read a file with an extension in (json, yaml, yml, text)'''
exts = ('.json', '.yaml', '.yml', '.txt')
def write_file(path, data, indent=4):
"""Read a file with an extension in (json, yaml, yml, text)"""
exts = (".json", ".yaml", ".yml", ".txt")
if not path:
print('Try to write empty file')
print("Try to write empty file")
path = Path(path)
path.parent.mkdir(parents=True, exist_ok=True)
if path.suffix not in exts:
print(f'Cannot read file {path}, extension must be in {exts}')
print(f"Cannot read file {path}, extension must be in {exts}")
return
if path.suffix.lower() in ('.yaml', '.yml'):
yaml = install_module('yaml')
if path.suffix.lower() in (".yaml", ".yml"):
yaml = install_module("yaml")
try:
path.write_text(yaml.dump(data), encoding='utf8')
path.write_text(yaml.dump(data), encoding="utf8")
except Exception as e:
print(e)
print(f'Could not write yaml file {path}')
print(f"Could not write yaml file {path}")
return
elif path.suffix.lower() == '.json':
elif path.suffix.lower() == ".json":
try:
path.write_text(json.dumps(data, indent=indent), encoding='utf8')
path.write_text(json.dumps(data, indent=indent), encoding="utf8")
except Exception as e:
print(e)
print(f'Could not write json file {path}')
print(f"Could not write json file {path}")
return
else:
data = path.write_text(data, encoding='utf8')
data = path.write_text(data, encoding="utf8")
def synchronize(src, dst, only_new=False, only_recent=False, clear=False):
#actionlib_dir = get_actionlib_dir(custom=custom)
#local_actionlib_dir = get_actionlib_dir(local=True, custom=custom)
# actionlib_dir = get_actionlib_dir(custom=custom)
# local_actionlib_dir = get_actionlib_dir(local=True, custom=custom)
try:
if clear and Path(dst).exists():
shutil.rmtree(dst)
#set_actionlib_dir(custom=custom)
# set_actionlib_dir(custom=custom)
script = Path(__file__).parent / 'synchronize.py'
script = Path(__file__).parent / "synchronize.py"
cmd = [
sys.executable,
script,
'--src', str(src),
'--dst', str(dst),
'--only-new', json.dumps(only_new),
'--only-recent', json.dumps(only_recent),
"--src",
str(src),
"--dst",
str(dst),
"--only-new",
json.dumps(only_new),
"--only-recent",
json.dumps(only_recent),
]
subprocess.Popen(cmd)
except Exception as e:
print(e)

View File

@ -11,7 +11,8 @@ import os
import re
import time
#from asset_library.constants import ASSETLIB_FILENAME
# from asset_library.constants import ASSETLIB_FILENAME
import inspect
from asset_library.common.file_utils import read_file
from asset_library.common.bl_utils import get_addon_prefs
@ -21,34 +22,37 @@ import bpy
def command(func):
'''Decorator to be used from printed functions argument and run time'''
func_name = func.__name__.replace('_', ' ').title()
"""Decorator to be used from printed functions argument and run time"""
func_name = func.__name__.replace("_", " ").title()
def _command(*args, **kargs):
bound = inspect.signature(func).bind(*args, **kargs)
bound.apply_defaults()
args_str = ', '.join([f'{k}={v}' for k, v in bound.arguments.items()])
print(f'\n[>-] {func_name} ({args_str}) --- Start ---')
args_str = ", ".join([f"{k}={v}" for k, v in bound.arguments.items()])
print(f"\n[>-] {func_name} ({args_str}) --- Start ---")
t0 = time.time()
result = func(*args, **kargs)
print(f'[>-] {func_name} --- Finished (total time : {time.time() - t0:.2f}s) ---')
print(
f"[>-] {func_name} --- Finished (total time : {time.time() - t0:.2f}s) ---"
)
return result
return _command
return _command
def asset_warning_callback(self, context):
"""Callback function to display a warning message when ading or modifying an asset"""
self.warning = ''
self.warning = ""
if not self.name:
self.warning = 'You need to specify a name'
self.warning = "You need to specify a name"
return
if not self.catalog:
self.warning = 'You need to specify a catalog'
self.warning = "You need to specify a catalog"
return
lib = get_active_library()
@ -60,30 +64,33 @@ def asset_warning_callback(self, context):
lib = prefs.libraries[lib.store_library]
if not lib.library_type.get_asset_path(self.name, self.catalog).parents[1].exists():
self.warning = 'A new folder will be created'
self.warning = "A new folder will be created"
def get_active_library():
'''Get the pref library properties from the active library of the asset browser'''
"""Get the pref library properties from the active library of the asset browser"""
prefs = get_addon_prefs()
asset_lib_ref = bpy.context.space_data.params.asset_library_ref
#Check for merged library
# Check for merged library
for l in prefs.libraries:
if l.library_name == asset_lib_ref:
return l
def get_active_catalog():
'''Get the active catalog path'''
"""Get the active catalog path"""
lib = get_active_library()
cat_data = lib.library_type.read_catalog()
cat_data = {v['id']:k for k,v in cat_data.items()}
cat_data = {v["id"]: k for k, v in cat_data.items()}
cat_id = bpy.context.space_data.params.catalog_id
if cat_id in cat_data:
return cat_data[cat_id]
return ''
return ""
"""
def norm_asset_datas(asset_file_datas):
@ -181,7 +188,7 @@ def get_asset_source(replace_local=False):
return source_path
"""
'''
"""
def get_catalog_path(filepath=None):
filepath = filepath or bpy.data.filepath
filepath = Path(filepath)
@ -196,7 +203,7 @@ def get_catalog_path(filepath=None):
catalog.touch(exist_ok=False)
return catalog
'''
"""
# def read_catalog(path, key='path'):
# cat_data = {}
@ -218,7 +225,7 @@ def get_catalog_path(filepath=None):
# cat_data[cat_path] = {'id':cat_id, 'name':cat_name}
# elif key =='name':
# cat_data[cat_name] = {'id':cat_id, 'path':cat_path}
# return cat_data
"""
def read_catalog(path):
@ -302,27 +309,29 @@ def create_catalog_file(json_path : str|Path, keep_existing_category : bool = Tr
return
"""
def clear_env_libraries():
print('clear_env_libraries')
print("clear_env_libraries")
prefs = get_addon_prefs()
asset_libraries = bpy.context.preferences.filepaths.asset_libraries
for env_lib in prefs.env_libraries:
name = env_lib.get('asset_library')
name = env_lib.get("asset_library")
if not name:
continue
asset_lib = asset_libraries.get(name)
if not asset_lib:
continue
index = list(asset_libraries).index(asset_lib)
bpy.ops.preferences.asset_library_remove(index=index)
prefs.env_libraries.clear()
'''
"""
env_libs = get_env_libraries()
paths = [Path(l['path']).resolve().as_posix() for n, l in env_libs.items()]
@ -331,16 +340,17 @@ def clear_env_libraries():
if (l.name in env_libs or lib_path in paths):
libs.remove(i)
'''
def set_env_libraries(path=None) -> list:
'''Read the environments variables and create the libraries'''
"""
#from asset_library.prefs import AssetLibraryOptions
def set_env_libraries(path=None) -> list:
"""Read the environments variables and create the libraries"""
# from asset_library.prefs import AssetLibraryOptions
prefs = get_addon_prefs()
path = path or prefs.config_directory
#print('Read', path)
# print('Read', path)
library_data = read_file(path)
clear_env_libraries()
@ -359,7 +369,8 @@ def set_env_libraries(path=None) -> list:
return libs
'''
"""
def get_env_libraries():
env_libraries = {}
@ -391,21 +402,17 @@ def get_env_libraries():
}
return env_libraries
'''
"""
def resync_lib(name, waiting_time):
bpy.app.timers.register(
lambda: bpy.ops.assetlib.synchronize(only_recent=True, name=name),
first_interval=waiting_time
first_interval=waiting_time,
)
'''
"""
def set_assetlib_paths():
prefs = bpy.context.preferences
@ -452,16 +459,4 @@ def set_actionlib_paths():
prefs.filepaths.asset_libraries[lib_id].name = actionlib_name
#prefs.filepaths.asset_libraries[lib_id].path = str(actionlib_dir)
'''
"""

View File

@ -10,7 +10,7 @@ class AssetCache:
def __init__(self, file_cache, data=None):
self.file_cache = file_cache
self.catalog = None
self.author = None
self.description = None
@ -32,10 +32,7 @@ class AssetCache:
@property
def metadata(self):
metadata = {
'.library_id': self.library_id,
'.filepath': self.filepath
}
metadata = {".library_id": self.library_id, ".filepath": self.filepath}
metadata.update(self._metadata)
@ -43,23 +40,23 @@ class AssetCache:
@property
def norm_name(self):
return self.name.replace(' ', '_').lower()
return self.name.replace(" ", "_").lower()
def unique_name(self):
return (self.filepath / self.name).as_posix()
def set_data(self, data):
catalog = data['catalog']
catalog = data["catalog"]
if isinstance(catalog, (list, tuple)):
catalog = '/'.join(catalog)
catalog = "/".join(catalog)
self.catalog = catalog
self.author = data.get('author', '')
self.description = data.get('description', '')
self.tags = data.get('tags', [])
self.type = data.get('type')
self.name = data['name']
self._metadata = data.get('metadata', {})
self.author = data.get("author", "")
self.description = data.get("description", "")
self.tags = data.get("tags", [])
self.type = data.get("type")
self.name = data["name"]
self._metadata = data.get("metadata", {})
def to_dict(self):
return dict(
@ -69,11 +66,11 @@ class AssetCache:
description=self.description,
tags=self.tags,
type=self.type,
name=self.name
name=self.name,
)
def __repr__(self):
return f'AssetCache(name={self.name}, catalog={self.catalog})'
return f"AssetCache(name={self.name}, catalog={self.catalog})"
def __eq__(self, other):
return self.to_dict() == other.to_dict()
@ -81,7 +78,7 @@ class AssetCache:
class AssetsCache:
def __init__(self, file_cache):
self.file_cache = file_cache
self._data = []
@ -111,12 +108,12 @@ class AssetsCache:
return next((a for a in self if a.name == name), None)
def __repr__(self):
return f'AssetsCache({list(self)})'
return f"AssetsCache({list(self)})"
class FileCache:
def __init__(self, library_cache, data=None):
self.library_cache = library_cache
self.filepath = None
@ -132,15 +129,15 @@ class FileCache:
def set_data(self, data):
if 'filepath' in data:
self.filepath = Path(data['filepath'])
if "filepath" in data:
self.filepath = Path(data["filepath"])
self.modified = data.get('modified', time.time_ns())
self.modified = data.get("modified", time.time_ns())
if data.get('type') == 'FILE':
if data.get("type") == "FILE":
self.assets.add(data)
for asset_cache_data in data.get('assets', []):
for asset_cache_data in data.get("assets", []):
self.assets.add(asset_cache_data)
def to_dict(self):
@ -148,7 +145,7 @@ class FileCache:
filepath=self.filepath.as_posix(),
modified=self.modified,
library_id=self.library_id,
assets=[asset_cache.to_dict() for asset_cache in self]
assets=[asset_cache.to_dict() for asset_cache in self],
)
def __iter__(self):
@ -158,14 +155,14 @@ class FileCache:
return self._data[key]
def __repr__(self):
return f'FileCache(filepath={self.filepath})'
return f"FileCache(filepath={self.filepath})"
class AssetCacheDiff:
def __init__(self, library_cache, asset_cache, operation):
self.library_cache = library_cache
#self.filepath = data['filepath']
# self.filepath = data['filepath']
self.operation = operation
self.asset_cache = asset_cache
@ -189,32 +186,49 @@ class LibraryCacheDiff:
self._data += new_asset_diffs
return new_asset_diffs
def compare(self, old_cache, new_cache):
if old_cache is None or new_cache is None:
print('Cannot Compare cache with None')
print("Cannot Compare cache with None")
cache_dict = {a.unique_name : a for a in old_cache.asset_caches}
new_cache_dict = {a.unique_name : a for a in new_cache.asset_caches}
cache_dict = {a.unique_name: a for a in old_cache.asset_caches}
new_cache_dict = {a.unique_name: a for a in new_cache.asset_caches}
assets_added = self.add([v for k, v in new_cache_dict.items() if k not in cache_dict], 'ADD')
assets_removed = self.add([v for k, v in cache_dict.items() if k not in new_cache_dict], 'REMOVED')
assets_modified = self.add([v for k, v in cache_dict.items() if v not in assets_removed and v!= new_cache_dict[k]], 'MODIFIED')
assets_added = self.add(
[v for k, v in new_cache_dict.items() if k not in cache_dict], "ADD"
)
assets_removed = self.add(
[v for k, v in cache_dict.items() if k not in new_cache_dict], "REMOVED"
)
assets_modified = self.add(
[
v
for k, v in cache_dict.items()
if v not in assets_removed and v != new_cache_dict[k]
],
"MODIFIED",
)
if assets_added:
print(f'{len(assets_added)} Assets Added \n{tuple(a.name for a in assets_added[:10])}...\n')
print(
f"{len(assets_added)} Assets Added \n{tuple(a.name for a in assets_added[:10])}...\n"
)
if assets_removed:
print(f'{len(assets_removed)} Assets Removed \n{tuple(a.name for a in assets_removed[:10])}...\n')
print(
f"{len(assets_removed)} Assets Removed \n{tuple(a.name for a in assets_removed[:10])}...\n"
)
if assets_modified:
print(f'{len(assets_modified)} Assets Modified \n{tuple(a.name for a in assets_modified[:10])}...\n')
print(
f"{len(assets_modified)} Assets Modified \n{tuple(a.name for a in assets_modified[:10])}...\n"
)
if len(self) == 0:
print('No change in the library')
print("No change in the library")
return self
def group_by(self, key):
'''Return groups of file cache diff using the key provided'''
"""Return groups of file cache diff using the key provided"""
data = list(self).sort(key=key)
return groupby(data, key=key)
@ -228,17 +242,17 @@ class LibraryCacheDiff:
return len(self._data)
def __repr__(self):
return f'LibraryCacheDiff(operations={[o for o in self][:2]}...)'
return f"LibraryCacheDiff(operations={[o for o in self][:2]}...)"
class LibraryCache:
def __init__(self, filepath):
self.filepath = Path(filepath)
self._data = []
@classmethod
def from_library(cls, library):
def from_library(cls, library):
filepath = library.library_path / f"blender_assets.{library.id}.json"
return cls(filepath)
@ -248,28 +262,28 @@ class LibraryCache:
@property
def library_id(self):
return self.filepath.stem.split('.')[-1]
return self.filepath.stem.split(".")[-1]
#@property
#def filepath(self):
# @property
# def filepath(self):
# """Get the filepath of the library json file relative to the library"""
# return self.directory / self.filename
def catalogs(self):
return set(a.catalog for a in self.asset_caches)
@property
def asset_caches(self):
'''Return an iterator to get all asset caches'''
"""Return an iterator to get all asset caches"""
return (asset_cache for file_cache in self for asset_cache in file_cache)
@property
def tmp_filepath(self):
return Path(bpy.app.tempdir) / self.filename
def read(self):
print(f'Read cache from {self.filepath}')
print(f"Read cache from {self.filepath}")
for file_cache_data in read_file(self.filepath):
self.add(file_cache_data)
@ -280,7 +294,7 @@ class LibraryCache:
if tmp:
filepath = self.tmp_filepath
print(f'Write cache file to {filepath}')
print(f"Write cache file to {filepath}")
write_file(filepath, self._data)
return filepath
@ -293,7 +307,7 @@ class LibraryCache:
def add_asset_cache(self, asset_cache_data, filepath=None):
if filepath is None:
filepath = asset_cache_data['filepath']
filepath = asset_cache_data["filepath"]
file_cache = self.get(filepath)
if not file_cache:
@ -334,28 +348,28 @@ class LibraryCache:
if new_cache is None:
new_cache = self
return LibraryCacheDiff(old_cache, new_cache)
return LibraryCacheDiff(old_cache, new_cache)
def update(self, cache_diff):
#Update the cache with the operations
# Update the cache with the operations
for asset_cache_diff in cache_diff:
file_cache = self.get(asset_cache_diff.filepath)
if not asset_cache:
print(f'Filepath {asset_cache_diff.filepath} not in {self}' )
print(f"Filepath {asset_cache_diff.filepath} not in {self}")
continue
asset_cache = file_cache.get(asset_cache_diff.name)
if not asset_cache:
print(f'Asset {asset_cache_diff.name} not in file_cache {file_cache}' )
print(f"Asset {asset_cache_diff.name} not in file_cache {file_cache}")
continue
if asset_cache_diff.operation == 'REMOVE':
if asset_cache_diff.operation == "REMOVE":
file_cache.assets.remove(asset_cache_diff.name)
elif asset_cache_diff.operation in ('MODIFY', 'ADD'):
elif asset_cache_diff.operation in ("MODIFY", "ADD"):
asset_cache.set_data(asset_cache_diff.asset_cache.to_dict())
return self
def __len__(self):
@ -363,7 +377,7 @@ class LibraryCache:
def __iter__(self):
return iter(self._data)
def __getitem__(self, key):
if isinstance(key, str):
return self.to_dict()[key]
@ -377,5 +391,4 @@ class LibraryCache:
return next((a for a in self if a.filepath == filepath), None)
def __repr__(self):
return f'LibraryCache(library_id={self.library_id})'
return f"LibraryCache(library_id={self.library_id})"

View File

@ -1,4 +1,3 @@
import argparse
import fnmatch
import importlib.util
@ -11,7 +10,7 @@ from pathlib import Path
# import module utils without excuting __init__
spec = importlib.util.spec_from_file_location(
"utils", Path(__file__).parent/"file_utils.py"
"utils", Path(__file__).parent / "file_utils.py"
)
utils = importlib.util.module_from_spec(spec)
spec.loader.exec_module(utils)
@ -19,24 +18,29 @@ spec.loader.exec_module(utils)
def synchronize(src, dst, only_new=False, only_recent=False):
excludes=['*.sync-conflict-*', '.*']
includes=['*.blend', 'blender_assets.cats.txt']
excludes = ["*.sync-conflict-*", ".*"]
includes = ["*.blend", "blender_assets.cats.txt"]
utils.copy_dir(
src, dst,
only_new=only_new, only_recent=only_recent,
excludes=excludes, includes=includes
src,
dst,
only_new=only_new,
only_recent=only_recent,
excludes=excludes,
includes=includes,
)
if __name__ == '__main__' :
parser = argparse.ArgumentParser(description='Add Comment To the tracker',
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Add Comment To the tracker",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
parser.add_argument('--src')
parser.add_argument('--dst')
parser.add_argument('--only-new', type=json.loads, default='false')
parser.add_argument('--only-recent', type=json.loads, default='false')
parser.add_argument("--src")
parser.add_argument("--dst")
parser.add_argument("--only-new", type=json.loads, default="false")
parser.add_argument("--only-recent", type=json.loads, default="false")
args = parser.parse_args()
synchronize(**vars(args))

View File

@ -9,51 +9,52 @@ import string
class TemplateFormatter(string.Formatter):
def format_field(self, value, format_spec):
if isinstance(value, str):
spec, sep = [*format_spec.split(':'), None][:2]
spec, sep = [*format_spec.split(":"), None][:2]
if sep:
value = value.replace('_', ' ')
value = value = re.sub(r'([a-z])([A-Z])', rf'\1{sep}\2', value)
value = value.replace(' ', sep)
if spec == 'u':
value = value.replace("_", " ")
value = value = re.sub(r"([a-z])([A-Z])", rf"\1{sep}\2", value)
value = value.replace(" ", sep)
if spec == "u":
value = value.upper()
elif spec == 'l':
elif spec == "l":
value = value.lower()
elif spec == 't':
elif spec == "t":
value = value.title()
return super().format(value, format_spec)
class Template:
field_pattern = re.compile(r'{(\w+)\*{0,2}}')
field_pattern_recursive = re.compile(r'{(\w+)\*{2}}')
field_pattern = re.compile(r"{(\w+)\*{0,2}}")
field_pattern_recursive = re.compile(r"{(\w+)\*{2}}")
def __init__(self, template):
#asset_data_path = Path(lib_path) / ASSETLIB_FILENAME
# asset_data_path = Path(lib_path) / ASSETLIB_FILENAME
self.raw = template
self.formatter = TemplateFormatter()
@property
def glob_pattern(self):
pattern = self.field_pattern_recursive.sub('**', self.raw)
pattern = self.field_pattern.sub('*', pattern)
pattern = self.field_pattern_recursive.sub("**", self.raw)
pattern = self.field_pattern.sub("*", pattern)
return pattern
@property
def re_pattern(self):
pattern = self.field_pattern_recursive.sub('([\\\w -_.\/]+)', self.raw)
pattern = self.field_pattern.sub('([\\\w -_.]+)', pattern)
pattern = pattern.replace('?', '.')
pattern = pattern.replace('*', '.*')
pattern = self.field_pattern_recursive.sub("([\\\w -_.\/]+)", self.raw)
pattern = self.field_pattern.sub("([\\\w -_.]+)", pattern)
pattern = pattern.replace("?", ".")
pattern = pattern.replace("*", ".*")
return re.compile(pattern)
@property
def fields(self):
return self.field_pattern.findall(self.raw)
#return [f or '0' for f in fields]
# return [f or '0' for f in fields]
def parse(self, path):
@ -61,7 +62,7 @@ class Template:
res = self.re_pattern.findall(path)
if not res:
print('Could not parse {path} with {self.re_pattern}')
print("Could not parse {path} with {self.re_pattern}")
return {}
fields = self.fields
@ -71,7 +72,7 @@ class Template:
else:
field_values = res[0]
return {k:v for k,v in zip(fields, field_values)}
return {k: v for k, v in zip(fields, field_values)}
def norm_data(self, data):
norm_data = {}
@ -81,7 +82,7 @@ class Template:
v = v.as_posix()
norm_data[k] = v
return norm_data
def format(self, data=None, **kargs):
@ -89,17 +90,17 @@ class Template:
data = {**(data or {}), **kargs}
try:
#print('FORMAT', self.raw, data)
# print('FORMAT', self.raw, data)
path = self.formatter.format(self.raw, **self.norm_data(data))
except KeyError as e:
print(f'Cannot format {self.raw} with {data}, field {e} is missing')
print(f"Cannot format {self.raw} with {data}, field {e} is missing")
return
path = os.path.expandvars(path)
return Path(path)
def glob(self, directory, pattern=None):
'''If pattern is given it need to be absolute'''
"""If pattern is given it need to be absolute"""
if pattern is None:
pattern = Path(directory, self.glob_pattern).as_posix()
@ -114,14 +115,14 @@ class Template:
pattern = self.format(data, **kargs)
pattern_str = str(pattern)
if '*' not in pattern_str and '?' not in pattern_str:
if "*" not in pattern_str and "?" not in pattern_str:
return pattern
paths = glob(pattern.as_posix())
if paths:
return Path(paths[0])
#return pattern
# return pattern
def __repr__(self):
return f'Template({self.raw})'
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'
#ADD_ASSET_DICT = {}
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()
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)
@ -33,65 +36,68 @@ def bundle_library(source_directory, bundle_directory, template_info, thumbnail_
rel_path = f.relative_to(source_directory).as_posix()
field_values = re.findall(re_pattern, rel_path)[0]
field_data = {k:v for k,v in zip(field_names, field_values)}
field_data = {k: v for k, v in zip(field_names, field_values)}
name = field_data.get('name', f.stem)
name = field_data.get("name", f.stem)
thumbnail = (f / thumbnail_template.format(name=name)).resolve()
asset_data = (f / template_info.format(name=name)).resolve()
catalogs = sorted([v for k,v in sorted(field_data.items()) if re.findall('cat[0-9]+', k)])
catalogs = [c.replace('_', ' ').title() for c in catalogs]
catalogs = sorted(
[v for k, v in sorted(field_data.items()) if re.findall("cat[0-9]+", k)]
)
catalogs = [c.replace("_", " ").title() for c in catalogs]
if not thumbnail.exists():
thumbnail_blend_file(f, thumbnail)
asset_data = {
'catalog' : '/'.join(catalogs),
'preview' : thumbnail.as_posix(), #'./' + bpy.path.relpath(str(thumbnail), start=str(f))[2:],
'filepath' : f.as_posix(), #'./' + bpy.path.relpath(str(f), start=str(asset_data_path))[2:],
'name': name,
'tags': [],
'metadata': {'filepath': f.as_posix()}
"catalog": "/".join(catalogs),
"preview": thumbnail.as_posix(), #'./' + bpy.path.relpath(str(thumbnail), start=str(f))[2:],
"filepath": f.as_posix(), #'./' + bpy.path.relpath(str(f), start=str(asset_data_path))[2:],
"name": name,
"tags": [],
"metadata": {"filepath": f.as_posix()},
}
asset_file_datas.append(asset_data)
# Write json data file to store all asset found
print(f'Writing asset data file to, {asset_data_path}')
print(f"Writing asset data file to, {asset_data_path}")
asset_data_path.write_text(json.dumps(asset_file_datas, indent=4))
#script = MODULE_DIR / 'common' / 'bundle_blend.py'
#cmd = [bpy.app.binary_path, '--python', str(script), '--', '--filepath', str(filepath)]
#print(cmd)
#subprocess.call(cmd)
# script = MODULE_DIR / 'common' / 'bundle_blend.py'
# cmd = [bpy.app.binary_path, '--python', str(script), '--', '--filepath', str(filepath)]
# print(cmd)
# subprocess.call(cmd)
@command
def bundle_blend(filepath, depth=0):
#print('Bundle Blend...')
# print('Bundle Blend...')
filepath = Path(filepath)
#asset_data_path = get_asset_datas_file(filepath)
# asset_data_path = get_asset_datas_file(filepath)
asset_data_path = filepath / ASSETLIB_FILENAME
blend_name = filepath.name.replace(' ', '_').lower()
blend_path = (filepath / blend_name).with_suffix('.blend')
blend_name = filepath.name.replace(" ", "_").lower()
blend_path = (filepath / blend_name).with_suffix(".blend")
if not asset_data_path.exists():
raise Exception(f'The file {asset_data_path} not exist')
raise Exception(f"The file {asset_data_path} not exist")
catalog_path = get_catalog_path(filepath)
catalog_data = read_catalog(catalog_path)
asset_file_data = json.loads(asset_data_path.read_text())
#asset_file_data = {i['catalog']:i for i in asset_file_data}
# asset_file_data = {i['catalog']:i for i in asset_file_data}
if depth == 0:
groups = [asset_file_data]
else:
asset_file_data.sort(key=lambda x :x['catalog'].split('/')[:depth])
groups = groupby(asset_file_data, key=lambda x :x['catalog'].split('/')[:depth])
asset_file_data.sort(key=lambda x: x["catalog"].split("/")[:depth])
groups = groupby(asset_file_data, key=lambda x: x["catalog"].split("/")[:depth])
#progress = 0
# progress = 0
total_assets = len(asset_file_data)
i = 0
@ -99,63 +105,63 @@ def bundle_blend(filepath, depth=0):
bpy.ops.wm.read_homefile(use_empty=True)
for asset_data in asset_datas:
blend_name = sub_path[-1].replace(' ', '_').lower()
blend_path = Path(filepath, *sub_path, blend_name).with_suffix('.blend')
blend_name = sub_path[-1].replace(" ", "_").lower()
blend_path = Path(filepath, *sub_path, blend_name).with_suffix(".blend")
if i % int(total_assets / 100) == 0:
print(f'Progress: {int(i / total_assets * 100)}')
print(f"Progress: {int(i / total_assets * 100)}")
col = bpy.data.collections.new(name=asset_data['name'])
col = bpy.data.collections.new(name=asset_data["name"])
# Seems slow
#bpy.context.scene.collection.children.link(col)
# bpy.context.scene.collection.children.link(col)
col.asset_mark()
with bpy.context.temp_override(id=col):
bpy.ops.ed.lib_id_load_custom_preview(
filepath=asset_data['preview']
)
bpy.ops.ed.lib_id_load_custom_preview(filepath=asset_data["preview"])
col.asset_data.description = asset_data.get('description', '')
col.asset_data.description = asset_data.get("description", "")
catalog_name = asset_data['catalog']
catalog_name = asset_data["catalog"]
catalog = catalog_data.get(catalog_name)
if not catalog:
catalog = {'id': str(uuid.uuid4()), 'name': catalog_name}
catalog = {"id": str(uuid.uuid4()), "name": catalog_name}
catalog_data[catalog_name] = catalog
col.asset_data.catalog_id = catalog['id']
col.asset_data.catalog_id = catalog["id"]
for k, v in asset_data.get('metadata', {}).items():
for k, v in asset_data.get("metadata", {}).items():
col.asset_data[k] = v
i += 1
print(f'Saving Blend to {blend_path}')
print(f"Saving Blend to {blend_path}")
blend_path.mkdir(exist_ok=True, parents=True)
bpy.ops.wm.save_as_mainfile(filepath=str(blend_path), compress=True)
write_catalog(catalog_path, catalog_data)
bpy.ops.wm.quit_blender()
if __name__ == '__main__' :
parser = argparse.ArgumentParser(description='bundle_blend',
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="bundle_blend",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
parser.add_argument('--source-path')
parser.add_argument('--bundle-path')
parser.add_argument('--asset-data-template')
parser.add_argument('--thumbnail-template')
parser.add_argument('--template', default=None)
parser.add_argument('--data-file', default=None)
parser.add_argument('--depth', default=0, type=int)
parser.add_argument("--source-path")
parser.add_argument("--bundle-path")
parser.add_argument("--asset-data-template")
parser.add_argument("--thumbnail-template")
parser.add_argument("--template", default=None)
parser.add_argument("--data-file", default=None)
parser.add_argument("--depth", default=0, type=int)
if '--' in sys.argv :
index = sys.argv.index('--')
sys.argv = [sys.argv[index-1], *sys.argv[index+1:]]
if "--" in sys.argv:
index = sys.argv.index("--")
sys.argv = [sys.argv[index - 1], *sys.argv[index + 1 :]]
args = parser.parse_args()
@ -165,6 +171,7 @@ if __name__ == '__main__' :
template_info=args.template_info,
thumbnail_template=args.thumbnail_template,
template=args.template,
data_file=args.data_file)
data_file=args.data_file,
)
bundle_blend(filepath=args.bundle_directory, depth=args.depth)
bundle_blend(filepath=args.bundle_directory, depth=args.depth)

View File

@ -1,4 +1,3 @@
import bpy
from pathlib import Path
@ -18,21 +17,23 @@ from asset_library.common.functions import get_active_library
def draw_context_menu(layout):
#asset = context.active_file
# asset = context.active_file
layout.operator_context = "INVOKE_DEFAULT"
lib = get_active_library()
filepath = lib.library_type.get_active_asset_path()
layout.operator("assetlib.open_blend_file", text="Open Blend File")#.filepath = asset.asset_data['filepath']
layout.operator(
"assetlib.open_blend_file", text="Open Blend File"
) # .filepath = asset.asset_data['filepath']
op = layout.operator("wm.link", text="Link")
op.filepath = str(filepath)
op = layout.operator("wm.append", text="Append")
op.filepath = str(filepath)
def draw_header(layout):
'''Draw the header of the Asset Browser Window'''
"""Draw the header of the Asset Browser Window"""
layout.separator()
#layout.operator("actionlib.store_anim_pose", text='Add Action', icon='FILE_NEW')
# 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))
@ -22,4 +23,4 @@ def register() -> None:
def unregister() -> None:
for km, kmi in addon_keymaps:
km.keymap_items.remove(kmi)
addon_keymaps.clear()
addon_keymaps.clear()

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,44 +16,44 @@ from asset_library.common.functions import get_active_library
class ASSETLIB_OT_open_blend_file(Operator):
bl_idname = "assetlib.open_blend_file"
bl_options = {"REGISTER", "UNDO"}
bl_label = 'Open Blender File'
bl_description = 'Open blender file'
bl_label = "Open Blender File"
bl_description = "Open blender file"
@classmethod
def poll(cls, context: Context) -> bool:
if not asset_utils.SpaceAssetInfo.is_asset_browser(context.space_data):
cls.poll_message_set("Current editor is not an asset browser")
return False
lib = get_active_library()
if not lib or lib.data_type != 'FILE':
if not lib or lib.data_type != "FILE":
return False
if not context.active_file or 'filepath' not in context.active_file.asset_data:
if not context.active_file or "filepath" not in context.active_file.asset_data:
cls.poll_message_set("Has not filepath property")
return False
return True
def execute(self, context: Context) -> Set[str]:
lib = get_active_library()
filepath = lib.library_type.get_active_asset_path()
open_blender_file(filepath)
return {'FINISHED'}
return {"FINISHED"}
classes = (ASSETLIB_OT_open_blend_file,)
classes = (
ASSETLIB_OT_open_blend_file,
)
def register():
for cls in classes:
bpy.utils.register_class(cls)
def unregister():
for cls in reversed(classes):
bpy.utils.unregister_class(cls)
bpy.utils.unregister_class(cls)

122
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,21 +114,23 @@ class ASSETLIB_PT_pose_library_usage(Panel):
col = layout.column(align=True)
row = col.row(align=True)
row.operator("actionlib.store_anim_pose", text='Store Anim/Pose', icon='ACTION')
'''
"""
class ASSETLIB_PT_pose_library_editing(PoseLibraryPanel, asset_utils.AssetBrowserPanel, Panel):
bl_space_type = 'FILE_BROWSER'
class ASSETLIB_PT_pose_library_editing(
PoseLibraryPanel, asset_utils.AssetBrowserPanel, Panel
):
bl_space_type = "FILE_BROWSER"
bl_region_type = "TOOL_PROPS"
bl_label = "Metadata"
#bl_options = {'HIDE_HEADER'}
# bl_options = {'HIDE_HEADER'}
# asset_categories = {'ANIMATIONS'}
@classmethod
def poll(cls, context: Context) -> bool:
sp = context.space_data
if not (sp and sp.type == 'FILE_BROWSER' and sp.browse_mode == 'ASSETS'):
if not (sp and sp.type == "FILE_BROWSER" and sp.browse_mode == "ASSETS"):
return False
if not (context.active_file and context.active_file.asset_data):
@ -144,34 +143,34 @@ class ASSETLIB_PT_pose_library_editing(PoseLibraryPanel, asset_utils.AssetBrowse
layout.use_property_split = True
asset_data = context.active_file.asset_data
metadata = ['camera', 'is_single_frame', 'rest_pose']
if 'camera' in asset_data.keys():
layout.prop(asset_data, f'["camera"]', text='Camera', icon='CAMERA_DATA')
if 'is_single_frame' in asset_data.keys():
layout.prop(asset_data, f'["is_single_frame"]', text='Is Single Frame')
if 'rest_pose' in asset_data.keys():
layout.prop(asset_data, f'["rest_pose"]', text='Rest Pose', icon='ACTION')
if 'filepath' in asset_data.keys():
layout.prop(asset_data, f'["filepath"]', text='Filepath')
metadata = ["camera", "is_single_frame", "rest_pose"]
if "camera" in asset_data.keys():
layout.prop(asset_data, f'["camera"]', text="Camera", icon="CAMERA_DATA")
if "is_single_frame" in asset_data.keys():
layout.prop(asset_data, f'["is_single_frame"]', text="Is Single Frame")
if "rest_pose" in asset_data.keys():
layout.prop(asset_data, f'["rest_pose"]', text="Rest Pose", icon="ACTION")
if "filepath" in asset_data.keys():
layout.prop(asset_data, f'["filepath"]', text="Filepath")
class ASSETLIB_MT_context_menu(AssetLibraryMenu, Menu):
bl_label = "Asset Library Menu"
@classmethod
def poll(cls, context):
if not asset_utils.SpaceAssetInfo.is_asset_browser(context.space_data):
cls.poll_message_set("Current editor is not an asset browser")
return False
prefs = get_addon_prefs()
asset_lib_ref = context.space_data.params.asset_library_ref
lib = get_active_library()
if not lib:
return False
return True
def draw(self, context):
@ -190,7 +189,7 @@ def is_option_region_visible(context, space):
return False
for region in context.area.regions:
if region.type == 'TOOL_PROPS' and region.width <= 1:
if region.type == "TOOL_PROPS" and region.width <= 1:
return False
return True
@ -209,10 +208,10 @@ def draw_assetbrowser_header(self, context):
row = self.layout.row(align=True)
row.separator()
row.operator("assetlib.bundle", icon='UV_SYNC_SELECT', text='').name = lib.name
#op
#op.clean = False
#op.only_recent = True
row.operator("assetlib.bundle", icon="UV_SYNC_SELECT", text="").name = lib.name
# op
# op.clean = False
# op.only_recent = True
lib.library_type.draw_header(row)
@ -224,7 +223,7 @@ def draw_assetbrowser_header(self, context):
sub = row.row()
sub.ui_units_x = 10
sub.prop(params, "filter_search", text="", icon='VIEWZOOM')
sub.prop(params, "filter_search", text="", icon="VIEWZOOM")
row.separator_spacer()
@ -239,17 +238,18 @@ def draw_assetbrowser_header(self, context):
row.operator(
"screen.region_toggle",
text="",
icon='PREFERENCES',
depress=is_option_region_visible(context, space_data)
).region_type = 'TOOL_PROPS'
icon="PREFERENCES",
depress=is_option_region_visible(context, space_data),
).region_type = "TOOL_PROPS"
### Messagebus subscription to monitor asset library changes.
_msgbus_owner = object()
def _on_asset_library_changed() -> None:
"""Update areas when a different asset library is selected."""
refresh_area_types = {'DOPESHEET_EDITOR', 'VIEW_3D'}
refresh_area_types = {"DOPESHEET_EDITOR", "VIEW_3D"}
for win in bpy.context.window_manager.windows:
for area in win.screen.areas:
if area.type not in refresh_area_types:
@ -257,6 +257,7 @@ def _on_asset_library_changed() -> None:
area.tag_redraw()
def register_message_bus() -> None:
bpy.msgbus.subscribe_rna(
@ -264,17 +265,20 @@ def register_message_bus() -> None:
owner=_msgbus_owner,
args=(),
notify=_on_asset_library_changed,
options={'PERSISTENT'},
options={"PERSISTENT"},
)
def unregister_message_bus() -> None:
bpy.msgbus.clear_by_owner(_msgbus_owner)
@bpy.app.handlers.persistent
def _on_blendfile_load_pre(none, other_none) -> None:
# The parameters are required, but both are None.
unregister_message_bus()
@bpy.app.handlers.persistent
def _on_blendfile_load_post(none, other_none) -> None:
# The parameters are required, but both are None.
@ -283,9 +287,9 @@ def _on_blendfile_load_post(none, other_none) -> None:
classes = (
ASSETLIB_PT_pose_library_editing,
#ASSETLIB_PT_pose_library_usage,
# ASSETLIB_PT_pose_library_usage,
ASSETLIB_MT_context_menu,
ASSETLIB_PT_libraries
ASSETLIB_PT_libraries,
)
@ -293,22 +297,26 @@ def register() -> None:
for cls in classes:
bpy.utils.register_class(cls)
bpy.types.FILEBROWSER_HT_header._draw_asset_browser_buttons = bpy.types.FILEBROWSER_HT_header.draw_asset_browser_buttons
bpy.types.FILEBROWSER_HT_header.draw_asset_browser_buttons = draw_assetbrowser_header
bpy.types.FILEBROWSER_HT_header._draw_asset_browser_buttons = (
bpy.types.FILEBROWSER_HT_header.draw_asset_browser_buttons
)
bpy.types.FILEBROWSER_HT_header.draw_asset_browser_buttons = (
draw_assetbrowser_header
)
#WorkSpace.active_pose_asset_index = bpy.props.IntProperty(
# WorkSpace.active_pose_asset_index = bpy.props.IntProperty(
# name="Active Pose Asset",
# # TODO explain which list the index belongs to, or how it can be used to get the pose.
# description="Per workspace index of the active pose asset"
#)
# )
# Register for window-manager. This is a global property that shouldn't be
# written to files.
#WindowManager.pose_assets = bpy.props.CollectionProperty(type=AssetHandle)
# WindowManager.pose_assets = bpy.props.CollectionProperty(type=AssetHandle)
# bpy.types.UI_MT_list_item_context_menu.prepend(pose_library_list_item_context_menu)
# bpy.types.ASSETLIB_MT_context_menu.prepend(pose_library_list_item_context_menu)
# bpy.types.ACTIONLIB_MT_context_menu.prepend(pose_library_list_item_context_menu)
#bpy.types.ASSETBROWSER_MT_editor_menus.append(draw_assetbrowser_header)
# bpy.types.ASSETBROWSER_MT_editor_menus.append(draw_assetbrowser_header)
register_message_bus()
bpy.app.handlers.load_pre.append(_on_blendfile_load_pre)
@ -319,15 +327,17 @@ def unregister() -> None:
for cls in reversed(classes):
bpy.utils.unregister_class(cls)
bpy.types.FILEBROWSER_HT_header.draw_asset_browser_buttons = bpy.types.FILEBROWSER_HT_header._draw_asset_browser_buttons
bpy.types.FILEBROWSER_HT_header.draw_asset_browser_buttons = (
bpy.types.FILEBROWSER_HT_header._draw_asset_browser_buttons
)
del bpy.types.FILEBROWSER_HT_header._draw_asset_browser_buttons
unregister_message_bus()
#del WorkSpace.active_pose_asset_index
#del WindowManager.pose_assets
# del WorkSpace.active_pose_asset_index
# del WindowManager.pose_assets
# bpy.types.UI_MT_list_item_context_menu.remove(pose_library_list_item_context_menu)
# bpy.types.ASSETLIB_MT_context_menu.remove(pose_library_list_item_context_menu)
# bpy.types.ACTIONLIB_MT_context_menu.remove(pose_library_list_item_context_menu)
#bpy.types.ASSETBROWSER_MT_editor_menus.remove(draw_assetbrowser_header)
# bpy.types.ASSETBROWSER_MT_editor_menus.remove(draw_assetbrowser_header)

View File

@ -12,18 +12,25 @@ addon_keymaps: List[Tuple[bpy.types.KeyMap, bpy.types.KeyMapItem]] = []
@persistent
def copy_play_anim(dummy):
wm = bpy.context.window_manager
km = wm.keyconfigs.addon.keymaps.new(name="File Browser Main", space_type="FILE_BROWSER")
km = wm.keyconfigs.addon.keymaps.new(
name="File Browser Main", space_type="FILE_BROWSER"
)
km_frames = wm.keyconfigs.user.keymaps.get('Frames')
km_frames = wm.keyconfigs.user.keymaps.get("Frames")
if km_frames:
play = km_frames.keymap_items.get('screen.animation_play')
play = km_frames.keymap_items.get("screen.animation_play")
if play:
kmi = km.keymap_items.new(
"assetlib.play_preview",
play.type, play.value,
any=play.any, shift=play.shift, ctrl=play.ctrl, alt=play.alt,
oskey=play.oskey, key_modifier=play.key_modifier,
)
"assetlib.play_preview",
play.type,
play.value,
any=play.any,
shift=play.shift,
ctrl=play.ctrl,
alt=play.alt,
oskey=play.oskey,
key_modifier=play.key_modifier,
)
addon_keymaps.append((km, kmi))
@ -33,10 +40,12 @@ def register() -> None:
# This happens when Blender is running in the background.
return
km = wm.keyconfigs.addon.keymaps.new(name="File Browser Main", space_type="FILE_BROWSER")
km = wm.keyconfigs.addon.keymaps.new(
name="File Browser Main", space_type="FILE_BROWSER"
)
kmi = km.keymap_items.new("wm.call_menu", "RIGHTMOUSE", "PRESS")
kmi.properties.name = 'ASSETLIB_MT_context_menu'
kmi.properties.name = "ASSETLIB_MT_context_menu"
addon_keymaps.append((km, kmi))
kmi = km.keymap_items.new("assetlib.play_preview", "SPACE", "PRESS")
@ -45,14 +54,15 @@ def register() -> None:
# km = addon.keymaps.new(name = "Grease Pencil Stroke Paint Mode", space_type = "EMPTY")
# kmi = km.keymap_items.new('wm.call_panel', type='F2', value='PRESS')
if 'copy_play_anim' not in [hand.__name__ for hand in bpy.app.handlers.load_post]:
if "copy_play_anim" not in [hand.__name__ for hand in bpy.app.handlers.load_post]:
bpy.app.handlers.load_post.append(copy_play_anim)
def unregister() -> None:
# Clear shortcuts from the keymap.
if 'copy_play_anim' in [hand.__name__ for hand in bpy.app.handlers.load_post]:
if "copy_play_anim" in [hand.__name__ for hand in bpy.app.handlers.load_post]:
bpy.app.handlers.load_post.remove(copy_play_anim)
for km, kmi in addon_keymaps:
km.keymap_items.remove(kmi)
addon_keymaps.clear()
addon_keymaps.clear()

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,27 +22,29 @@ from pprint import pprint
class Conform(ScanFolder):
name = "Conform"
source_directory : StringProperty(subtype='DIR_PATH')
source_directory: StringProperty(subtype="DIR_PATH")
target_template_file : StringProperty()
target_template_info : StringProperty()
target_template_image : StringProperty()
target_template_video : StringProperty()
target_template_file: StringProperty()
target_template_info: StringProperty()
target_template_image: StringProperty()
target_template_video: StringProperty()
def draw_prefs(self, layout):
layout.prop(self, "source_directory", text="Source : Directory")
col = layout.column(align=True)
col.prop(self, "source_template_file", icon='COPY_ID', text='Template file')
col.prop(self, "source_template_image", icon='COPY_ID', text='Template image')
col.prop(self, "source_template_video", icon='COPY_ID', text='Template video')
col.prop(self, "source_template_info", icon='COPY_ID', text='Template info')
col.prop(self, "source_template_file", icon="COPY_ID", text="Template file")
col.prop(self, "source_template_image", icon="COPY_ID", text="Template image")
col.prop(self, "source_template_video", icon="COPY_ID", text="Template video")
col.prop(self, "source_template_info", icon="COPY_ID", text="Template info")
col = layout.column(align=True)
col.prop(self, "target_template_file", icon='COPY_ID', text='Target : Template file')
col.prop(self, "target_template_image", icon='COPY_ID', text='Template image')
col.prop(self, "target_template_video", icon='COPY_ID', text='Template video')
col.prop(self, "target_template_info", icon='COPY_ID', text='Template info')
col.prop(
self, "target_template_file", icon="COPY_ID", text="Target : Template file"
)
col.prop(self, "target_template_image", icon="COPY_ID", text="Template image")
col.prop(self, "target_template_video", icon="COPY_ID", text="Template video")
col.prop(self, "target_template_info", icon="COPY_ID", text="Template info")
def get_asset_bundle_path(self, asset_data):
"""Template file are relative"""
@ -52,48 +52,50 @@ class Conform(ScanFolder):
src_directory = Path(self.source_directory).resolve()
src_template_file = Template(self.source_template_file)
asset_path = Path(asset_data['filepath']).as_posix()
asset_path = Path(asset_data["filepath"]).as_posix()
asset_path = self.format_path(asset_path)
rel_path = asset_path.relative_to(src_directory).as_posix()
field_data = src_template_file.parse(rel_path)
#field_data = {f"catalog_{k}": v for k, v in field_data.items()}
# field_data = {f"catalog_{k}": v for k, v in field_data.items()}
# Change the int in the template by string to allow format
#target_template_file = re.sub(r'{(\d+)}', r'{cat\1}', self.target_template_file)
# Change the int in the template by string to allow format
# target_template_file = re.sub(r'{(\d+)}', r'{cat\1}', self.target_template_file)
format_data = self.format_asset_data(asset_data)
#format_data['asset_name'] = format_data['asset_name'].lower().replace(' ', '_')
# format_data['asset_name'] = format_data['asset_name'].lower().replace(' ', '_')
path = Template(self.target_template_file).format(format_data, **field_data).with_suffix('.blend')
path = (
Template(self.target_template_file)
.format(format_data, **field_data)
.with_suffix(".blend")
)
path = Path(self.bundle_directory, path).resolve()
return path
def set_asset_preview(self, asset, asset_data):
'''Load an externalize image as preview for an asset using the target template'''
"""Load an externalize image as preview for an asset using the target template"""
image_template = self.target_template_image
if not image_template:
return
asset_path = self.get_asset_bundle_path(asset_data)
asset_path = self.get_asset_bundle_path(asset_data)
image_path = self.find_path(image_template, asset_data, filepath=asset_path)
if image_path:
with bpy.context.temp_override(id=asset):
bpy.ops.ed.lib_id_load_custom_preview(
filepath=str(image_path)
)
bpy.ops.ed.lib_id_load_custom_preview(filepath=str(image_path))
else:
print(f'No image found for {image_template} on {asset.name}')
print(f"No image found for {image_template} on {asset.name}")
if asset.preview:
return asset.preview
def generate_previews(self, cache_diff):
print('Generate previews...')
print("Generate previews...")
# if cache in (None, ''):
# cache = self.fetch()
@ -103,12 +105,10 @@ class Conform(ScanFolder):
if isinstance(cache, (Path, str)):
cache_diff = LibraryCacheDiff(cache_diff)
#TODO Support all multiple data_type
# TODO Support all multiple data_type
for asset_info in cache:
if asset_info.get('type', self.data_type) == 'FILE':
if asset_info.get("type", self.data_type) == "FILE":
self.generate_blend_preview(asset_info)
else:
self.generate_asset_preview(asset_info)
@ -116,49 +116,55 @@ class Conform(ScanFolder):
def generate_asset_preview(self, asset_info):
"""Only generate preview when conforming a library"""
#print('\ngenerate_preview', asset_info['filepath'])
# print('\ngenerate_preview', asset_info['filepath'])
scn = bpy.context.scene
vl = bpy.context.view_layer
#Creating the preview for collection, object or material
#camera = scn.camera
# Creating the preview for collection, object or material
# camera = scn.camera
data_type = self.data_type #asset_info['data_type']
asset_path = self.format_path(asset_info['filepath'])
data_type = self.data_type # asset_info['data_type']
asset_path = self.format_path(asset_info["filepath"])
# Check if a source video exists and if so copying it in the new directory
if self.source_template_video and self.target_template_video:
for asset_data in asset_info['assets']:
for asset_data in asset_info["assets"]:
asset_data = dict(asset_data, filepath=asset_path)
dst_asset_path = self.get_asset_bundle_path(asset_data)
dst_video_path = self.format_path(self.target_template_video, asset_data, filepath=dst_asset_path)
dst_video_path = self.format_path(
self.target_template_video, asset_data, filepath=dst_asset_path
)
if dst_video_path.exists():
print(f'The dest video {dst_video_path} already exist')
print(f"The dest video {dst_video_path} already exist")
continue
src_video_path = self.find_path(self.source_template_video, asset_data)
if src_video_path:
print(f'Copy video from {src_video_path} to {dst_video_path}')
print(f"Copy video from {src_video_path} to {dst_video_path}")
self.copy_file(src_video_path, dst_video_path)
# Check if asset as a preview image or need it to be generated
asset_data_names = {}
if self.target_template_image:
for asset_data in asset_info['assets']:
for asset_data in asset_info["assets"]:
asset_data = dict(asset_data, filepath=asset_path)
name = asset_data['name']
name = asset_data["name"]
dst_asset_path = self.get_asset_bundle_path(asset_data)
dst_image_path = self.format_path(self.target_template_image, asset_data, filepath=dst_asset_path)
dst_image_path = self.format_path(
self.target_template_image, asset_data, filepath=dst_asset_path
)
if dst_image_path.exists():
print(f'The dest image {dst_image_path} already exist')
print(f"The dest image {dst_image_path} already exist")
continue
# Check if a source image exists and if so copying it in the new directory
if self.source_template_image:
src_image_path = self.find_path(self.source_template_image, asset_data)
src_image_path = self.find_path(
self.source_template_image, asset_data
)
if src_image_path:
if src_image_path.suffix == dst_image_path.suffix:
@ -170,47 +176,50 @@ class Conform(ScanFolder):
continue
#Store in a dict all asset_data that does not have preview
# Store in a dict all asset_data that does not have preview
asset_data_names[name] = dict(asset_data, image_path=dst_image_path)
if not asset_data_names:# No preview to generate
if not asset_data_names: # No preview to generate
return
print('Making Preview for', list(asset_data_names.keys()))
print("Making Preview for", list(asset_data_names.keys()))
asset_names = list(asset_data_names.keys())
assets = self.load_datablocks(asset_path, names=asset_names, link=True, type=data_type)
assets = self.load_datablocks(
asset_path, names=asset_names, link=True, type=data_type
)
for asset in assets:
if not asset:
continue
asset_data = asset_data_names[asset.name]
image_path = asset_data['image_path']
image_path = asset_data["image_path"]
if asset.preview:
print(f'Writing asset preview to {image_path}')
print(f"Writing asset preview to {image_path}")
self.write_preview(asset.preview, image_path)
continue
if data_type == 'COLLECTION':
if data_type == "COLLECTION":
bpy.ops.object.collection_instance_add(name=asset.name)
bpy.ops.view3d.camera_to_view_selected()
instance = vl.objects.active
#scn.collection.children.link(asset)
# scn.collection.children.link(asset)
scn.render.filepath = str(image_path)
print(f'Render asset {asset.name} to {image_path}')
print(f"Render asset {asset.name} to {image_path}")
bpy.ops.render.render(write_still=True)
#instance.user_clear()
# instance.user_clear()
asset.user_clear()
bpy.data.objects.remove(instance)
bpy.ops.outliner.orphans_purge(do_local_ids=True, do_linked_ids=True, do_recursive=True)
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
@ -15,32 +13,29 @@ class CopyFolder(LibraryType):
"""Copy library folder from a server to a local disk for better performance"""
name = "Copy Folder"
source_directory : StringProperty()
includes : StringProperty()
excludes : StringProperty()
source_directory: StringProperty()
includes: StringProperty()
excludes: StringProperty()
def bundle(self, cache_diff=None):
src = expandvars(self.source_directory)
dst = expandvars(self.bundle_directory)
includes = [inc.strip() for inc in self.includes.split(',')]
excludes = [ex.strip() for ex in self.excludes.split(',')]
includes = [inc.strip() for inc in self.includes.split(",")]
excludes = [ex.strip() for ex in self.excludes.split(",")]
print(f"Copy Folder from {src} to {dst}...")
copy_dir(src, dst, only_recent=True, excludes=excludes, includes=includes)
print(f'Copy Folder from {src} to {dst}...')
copy_dir(
src, dst, only_recent=True,
excludes=excludes, includes=includes
)
def filter_prop(self, prop):
if prop in ('template_info', 'template_video', 'template_image', 'blend_depth'):
if prop in ("template_info", "template_video", "template_image", "blend_depth"):
return False
return True
# def draw_prop(self, layout, prop):
# if prop in ('template_info', 'template_video', 'template_image', 'blend_depth'):
# return
# super().draw_prop(layout)
# super().draw_prop(layout)

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
@ -25,73 +23,75 @@ import time
class Kitsu(LibraryType):
name = "Kitsu"
template_name : StringProperty()
template_file : StringProperty()
source_directory : StringProperty(subtype='DIR_PATH')
#blend_depth: IntProperty(default=1)
source_template_image : StringProperty()
target_template_image : StringProperty()
template_name: StringProperty()
template_file: StringProperty()
source_directory: StringProperty(subtype="DIR_PATH")
# blend_depth: IntProperty(default=1)
source_template_image: StringProperty()
target_template_image: StringProperty()
url: StringProperty()
login: StringProperty()
password: StringProperty(subtype='PASSWORD')
password: StringProperty(subtype="PASSWORD")
project_name: StringProperty()
def connect(self, url=None, login=None, password=None):
'''Connect to kitsu api using provided url, login and password'''
gazu = install_module('gazu')
def connect(self, url=None, login=None, password=None):
"""Connect to kitsu api using provided url, login and password"""
gazu = install_module("gazu")
urllib3.disable_warnings()
if not self.url:
print(f'Kitsu Url: {self.url} is empty')
print(f"Kitsu Url: {self.url} is empty")
return
url = self.url
if not url.endswith('/api'):
url += '/api'
if not url.endswith("/api"):
url += "/api"
print(f'Info: Setting Host for kitsu {url}')
print(f"Info: Setting Host for kitsu {url}")
gazu.client.set_host(url)
if not gazu.client.host_is_up():
print('Error: Kitsu Host is down')
print("Error: Kitsu Host is down")
try:
print(f'Info: Log in to kitsu as {self.login}')
print(f"Info: Log in to kitsu as {self.login}")
res = gazu.log_in(self.login, self.password)
print(f'Info: Sucessfully login to Kitsu as {res["user"]["full_name"]}')
return res['user']
return res["user"]
except Exception as e:
print(f'Error: {traceback.format_exc()}')
print(f"Error: {traceback.format_exc()}")
def get_asset_path(self, name, catalog, directory=None):
directory = directory or self.source_directory
return Path(directory, self.get_asset_relative_path(name, catalog))
def get_asset_info(self, data, asset_path):
modified = time.time_ns()
catalog = data['entity_type_name'].title()
asset_path = self.prop_rel_path(asset_path, 'source_directory')
#asset_name = self.norm_file_name(data['name'])
catalog = data["entity_type_name"].title()
asset_path = self.prop_rel_path(asset_path, "source_directory")
# asset_name = self.norm_file_name(data['name'])
asset_info = dict(
filepath=asset_path,
modified=modified,
library_id=self.library.id,
assets=[dict(
catalog=catalog,
metadata=data.get('data', {}),
description=data['description'],
tags=[],
type=self.data_type,
#image=self.library.template_image,
#video=self.library.template_video,
name=data['name'])
]
assets=[
dict(
catalog=catalog,
metadata=data.get("data", {}),
description=data["description"],
tags=[],
type=self.data_type,
# image=self.library.template_image,
# video=self.library.template_video,
name=data["name"],
)
],
)
return asset_info
# def bundle(self, cache_diff=None):
@ -100,37 +100,36 @@ class Kitsu(LibraryType):
# return super().bundle(cache_diff=cache_diff)
def set_asset_preview(self, asset, asset_data):
'''Load an externalize image as preview for an asset using the source template'''
"""Load an externalize image as preview for an asset using the source template"""
asset_path = self.format_path(Path(asset_data['filepath']).as_posix())
asset_path = self.format_path(Path(asset_data["filepath"]).as_posix())
image_path = self.find_path(self.target_template_image, asset_data, filepath=asset_path)
image_path = self.find_path(
self.target_template_image, asset_data, filepath=asset_path
)
if image_path:
with bpy.context.temp_override(id=asset):
bpy.ops.ed.lib_id_load_custom_preview(
filepath=str(image_path)
)
bpy.ops.ed.lib_id_load_custom_preview(filepath=str(image_path))
else:
print(f'No image found for {self.target_template_image} on {asset.name}')
print(f"No image found for {self.target_template_image} on {asset.name}")
if asset.preview:
return asset.preview
def generate_previews(self, cache=None):
print('Generate previews...')
if cache in (None, ''):
print("Generate previews...")
if cache in (None, ""):
cache = self.fetch()
elif isinstance(cache, (Path, str)):
cache = self.read_cache(cache)
#TODO Support all multiple data_type
# TODO Support all multiple data_type
for asset_info in cache:
if asset_info.get('type', self.data_type) == 'FILE':
if asset_info.get("type", self.data_type) == "FILE":
self.generate_blend_preview(asset_info)
else:
self.generate_asset_preview(asset_info)
@ -141,35 +140,38 @@ class Kitsu(LibraryType):
scn = bpy.context.scene
vl = bpy.context.view_layer
asset_path = self.format_path(asset_info['filepath'])
asset_path = self.format_path(asset_info["filepath"])
lens = 85
if not asset_path.exists():
print(f'Blend file {asset_path} not exit')
print(f"Blend file {asset_path} not exit")
return
asset_data_names = {}
# First check wich assets need a preview
for asset_data in asset_info['assets']:
name = asset_data['name']
image_path = self.format_path(self.target_template_image, asset_data, filepath=asset_path)
for asset_data in asset_info["assets"]:
name = asset_data["name"]
image_path = self.format_path(
self.target_template_image, asset_data, filepath=asset_path
)
if image_path.exists():
continue
#Store in a dict all asset_data that does not have preview
# Store in a dict all asset_data that does not have preview
asset_data_names[name] = dict(asset_data, image_path=image_path)
if not asset_data_names:
print(f'All previews already existing for {asset_path}')
print(f"All previews already existing for {asset_path}")
return
#asset_names = [a['name'] for a in asset_info['assets']]
# asset_names = [a['name'] for a in asset_info['assets']]
asset_names = list(asset_data_names.keys())
assets = self.load_datablocks(asset_path, names=asset_names, link=True, type=data_type)
assets = self.load_datablocks(
asset_path, names=asset_names, link=True, type=data_type
)
print(asset_names)
print(assets)
@ -177,93 +179,106 @@ class Kitsu(LibraryType):
for asset in assets:
if not asset:
continue
print(f'Generate Preview for asset {asset.name}')
print(f"Generate Preview for asset {asset.name}")
asset_data = asset_data_names[asset.name]
#print(self.target_template_image, asset_path)
image_path = self.format_path(self.target_template_image, asset_data, filepath=asset_path)
# print(self.target_template_image, asset_path)
image_path = self.format_path(
self.target_template_image, asset_data, filepath=asset_path
)
# Force redo preview
# if asset.preview:
# print(f'Writing asset preview to {image_path}')
# self.write_preview(asset.preview, image_path)
# continue
if data_type == 'COLLECTION':
if data_type == "COLLECTION":
bpy.ops.object.collection_instance_add(name=asset.name)
scn.camera.data.lens = lens
bpy.ops.view3d.camera_to_view_selected()
scn.camera.data.lens -= 5
instance = vl.objects.active
#scn.collection.children.link(asset)
# scn.collection.children.link(asset)
scn.render.filepath = str(image_path)
scn.render.image_settings.file_format = self.format_from_ext(image_path.suffix)
scn.render.image_settings.color_mode = 'RGBA'
scn.render.image_settings.file_format = self.format_from_ext(
image_path.suffix
)
scn.render.image_settings.color_mode = "RGBA"
scn.render.image_settings.quality = 90
print(f'Render asset {asset.name} to {image_path}')
print(f"Render asset {asset.name} to {image_path}")
bpy.ops.render.render(write_still=True)
#instance.user_clear()
# instance.user_clear()
asset.user_clear()
bpy.data.objects.remove(instance)
bpy.ops.outliner.orphans_purge(do_local_ids=True, do_linked_ids=True, do_recursive=True)
bpy.ops.outliner.orphans_purge(
do_local_ids=True, do_linked_ids=True, do_recursive=True
)
def fetch(self):
"""Gather in a list all assets found in the folder"""
print(f'Fetch Assets for {self.library.name}')
print(f"Fetch Assets for {self.library.name}")
gazu = install_module('gazu')
gazu = install_module("gazu")
self.connect()
template_file = Template(self.template_file)
template_name = Template(self.template_name)
project = gazu.client.fetch_first('projects', {'name': self.project_name})
entity_types = gazu.client.fetch_all('entity-types')
entity_types_ids = {e['id']: e['name'] for e in entity_types}
project = gazu.client.fetch_first("projects", {"name": self.project_name})
entity_types = gazu.client.fetch_all("entity-types")
entity_types_ids = {e["id"]: e["name"] for e in entity_types}
cache = self.read_cache()
for asset_data in gazu.asset.all_assets_for_project(project):
asset_data['entity_type_name'] = entity_types_ids[asset_data.pop('entity_type_id')]
asset_name = asset_data['name']
asset_data["entity_type_name"] = entity_types_ids[
asset_data.pop("entity_type_id")
]
asset_name = asset_data["name"]
asset_field_data = dict(asset_name=asset_name, type=asset_data['entity_type_name'], source_directory=self.source_directory)
asset_field_data = dict(
asset_name=asset_name,
type=asset_data["entity_type_name"],
source_directory=self.source_directory,
)
try:
asset_field_data.update(template_name.parse(asset_name))
except Exception:
print(f'Warning: Could not parse {asset_name} with template {template_name}')
print(
f"Warning: Could not parse {asset_name} with template {template_name}"
)
asset_path = template_file.find(asset_field_data)
if not asset_path:
print(f'Warning: Could not find file for {template_file.format(asset_field_data)}')
print(
f"Warning: Could not find file for {template_file.format(asset_field_data)}"
)
continue
asset_path = self.prop_rel_path(asset_path, 'source_directory')
asset_path = self.prop_rel_path(asset_path, "source_directory")
asset_cache_data = dict(
catalog=asset_data['entity_type_name'].title(),
metadata=asset_data.get('data', {}),
description=asset_data['description'],
catalog=asset_data["entity_type_name"].title(),
metadata=asset_data.get("data", {}),
description=asset_data["description"],
tags=[],
type=self.data_type,
name=asset_data['name']
name=asset_data["name"],
)
cache.add_asset_cache(asset_cache_data, filepath=asset_path)
return cache

View File

@ -1,11 +1,10 @@
#from asset_library.common.functions import (norm_asset_datas,)
# from asset_library.common.functions import (norm_asset_datas,)
from asset_library.common.bl_utils import get_addon_prefs, load_datablocks
from asset_library.common.file_utils import read_file, write_file
from asset_library.common.template import Template
from asset_library.constants import (MODULE_DIR, RESOURCES_DIR)
from asset_library.constants import MODULE_DIR, RESOURCES_DIR
from asset_library import (action, collection, file)
from asset_library import action, collection, file
from asset_library.common.library_cache import LibraryCacheDiff
from bpy.types import PropertyGroup
@ -28,21 +27,21 @@ from copy import deepcopy
class LibraryType(PropertyGroup):
#def __init__(self):
# def __init__(self):
name = "Base Adapter"
#library = None
# library = None
@property
def library(self):
prefs = self.addon_prefs
for lib in prefs.libraries:
if lib.library_type == self:
return lib
@property
def bundle_directory(self):
return self.library.library_path
@property
def data_type(self):
return self.library.data_type
@ -78,24 +77,32 @@ class LibraryType(PropertyGroup):
@property
def addon_prefs(self):
return get_addon_prefs()
@property
def module_type(self):
lib_type = self.library.data_type
if lib_type == 'ACTION':
if lib_type == "ACTION":
return action
elif lib_type == 'FILE':
elif lib_type == "FILE":
return file
elif lib_type == 'COLLECTION':
elif lib_type == "COLLECTION":
return collection
@property
def format_data(self):
"""Dict for formating template"""
return dict(self.to_dict(), bundle_dir=self.library.bundle_dir, parent=self.library.parent)
return dict(
self.to_dict(),
bundle_dir=self.library.bundle_dir,
parent=self.library.parent,
)
def to_dict(self):
return {p: getattr(self, p) for p in self.bl_rna.properties.keys() if p !='rna_type'}
return {
p: getattr(self, p)
for p in self.bl_rna.properties.keys()
if p != "rna_type"
}
def read_catalog(self):
return self.library.read_catalog()
@ -104,10 +111,10 @@ class LibraryType(PropertyGroup):
return self.library.read_cache(filepath=filepath)
def fetch(self):
raise Exception('This method need to be define in the library_type')
raise Exception("This method need to be define in the library_type")
def norm_file_name(self, name):
return name.replace(' ', '_')
return name.replace(" ", "_")
def read_file(self, file):
return read_file(file)
@ -120,42 +127,46 @@ class LibraryType(PropertyGroup):
dst = Path(destination)
if not src.exists():
print(f'Cannot copy file {src}: file not exist')
print(f"Cannot copy file {src}: file not exist")
return
dst.parent.mkdir(exist_ok=True, parents=True)
if src == dst:
print(f'Cannot copy file {src}: source and destination are the same')
print(f"Cannot copy file {src}: source and destination are the same")
return
print(f'Copy file from {src} to {dst}')
print(f"Copy file from {src} to {dst}")
shutil.copy2(str(src), str(dst))
def load_datablocks(self, src, names=None, type='objects', link=True, expr=None, assets_only=False):
def load_datablocks(
self, src, names=None, type="objects", link=True, expr=None, assets_only=False
):
"""Link or append a datablock from a blendfile"""
if type.isupper():
type = f'{type.lower()}s'
type = f"{type.lower()}s"
return load_datablocks(src, names=names, type=type, link=link, expr=expr, assets_only=assets_only)
return load_datablocks(
src, names=names, type=type, link=link, expr=expr, assets_only=assets_only
)
def get_asset_data(self, asset):
"""Extract asset information on a datablock"""
return dict(
name=asset.name,
type=asset.bl_rna.name.upper(),
author=asset.asset_data.author,
tags=list(asset.asset_data.tags.keys()),
metadata=dict(asset.asset_data),
description=asset.asset_data.description,
)
name=asset.name,
type=asset.bl_rna.name.upper(),
author=asset.asset_data.author,
tags=list(asset.asset_data.tags.keys()),
metadata=dict(asset.asset_data),
description=asset.asset_data.description,
)
def get_asset_relative_path(self, name, catalog):
'''Get a relative path for the asset'''
"""Get a relative path for the asset"""
name = self.norm_file_name(name)
return Path(catalog, name, name).with_suffix('.blend')
return Path(catalog, name, name).with_suffix(".blend")
def get_active_asset_library(self):
prefs = get_addon_prefs()
@ -165,27 +176,27 @@ class LibraryType(PropertyGroup):
return self
lib = None
if '.library_id' in asset_handle.asset_data:
lib_id = asset_handle.asset_data['.library_id']
if ".library_id" in asset_handle.asset_data:
lib_id = asset_handle.asset_data[".library_id"]
lib = next((l for l in prefs.libraries if l.id == lib_id), None)
if not lib:
print(f"No library found for id {lib_id}")
if not lib:
lib = self
return lib
def get_active_asset_path(self):
'''Get the full path of the active asset_handle from the asset brower'''
"""Get the full path of the active asset_handle from the asset brower"""
prefs = get_addon_prefs()
asset_handle = bpy.context.asset_file_handle
lib = self.get_active_asset_library()
if 'filepath' in asset_handle.asset_data:
asset_path = asset_handle.asset_data['filepath']
if "filepath" in asset_handle.asset_data:
asset_path = asset_handle.asset_data["filepath"]
asset_path = lib.library_type.format_path(asset_path)
else:
asset_path = bpy.types.AssetHandle.get_full_library_path(
@ -195,43 +206,43 @@ class LibraryType(PropertyGroup):
return asset_path
def generate_previews(self):
raise Exception('Need to be defined in the library_type')
raise Exception("Need to be defined in the library_type")
def get_image_path(self, name, catalog, filepath):
raise Exception('Need to be defined in the library_type')
raise Exception("Need to be defined in the library_type")
def get_video_path(self, name, catalog, filepath):
raise Exception('Need to be defined in the library_type')
raise Exception("Need to be defined in the library_type")
def new_asset(self, asset, asset_cache):
raise Exception('Need to be defined in the library_type')
raise Exception("Need to be defined in the library_type")
def remove_asset(self, asset, asset_cache):
raise Exception('Need to be defined in the library_type')
raise Exception("Need to be defined in the library_type")
def set_asset_preview(self, asset, asset_cache):
raise Exception('Need to be defined in the library_type')
raise Exception("Need to be defined in the library_type")
def format_asset_data(self, data):
"""Get a dict for use in template fields"""
return {
'asset_name': data['name'],
'asset_path': Path(data['filepath']),
'catalog': data['catalog'],
'catalog_name': data['catalog'].replace('/', '_'),
"asset_name": data["name"],
"asset_path": Path(data["filepath"]),
"catalog": data["catalog"],
"catalog_name": data["catalog"].replace("/", "_"),
}
def format_path(self, template, data={}, **kargs):
if not template:
return None
if data:
data = self.format_asset_data(dict(data, **kargs))
else:
data = kargs
if template.startswith('.'): #the template is relative
template = Path(data['asset_path'], template).as_posix()
if template.startswith("."): # the template is relative
template = Path(data["asset_path"], template).as_posix()
params = dict(
**data,
@ -261,13 +272,9 @@ class LibraryType(PropertyGroup):
Path(asset_path).parent.mkdir(exist_ok=True, parents=True)
bpy.data.libraries.write(
str(asset_path),
{asset},
path_remap="NONE",
fake_user=True,
compress=True
str(asset_path), {asset}, path_remap="NONE", fake_user=True, compress=True
)
# def read_catalog(self, directory=None):
# """Read the catalog file of the library target directory or of the specified directory"""
# catalog_path = self.get_catalog_path(directory)
@ -283,12 +290,12 @@ class LibraryType(PropertyGroup):
# cat_id, cat_path, cat_name = line.split(':')
# cat_data[cat_path] = {'id':cat_id, 'name':cat_name}
# return cat_data
# def write_catalog(self, catalog_data, directory=None):
# """Write the catalog file in the library target directory or of the specified directory"""
# catalog_path = self.get_catalog_path(directory)
# lines = ['VERSION 1', '']
@ -300,13 +307,13 @@ class LibraryType(PropertyGroup):
# for p in Path(cat_path).parents[:-1]:
# if p in cat_data or p in norm_data:
# continue
# norm_data[p.as_posix()] = {'id': str(uuid.uuid4()), 'name': '-'.join(p.parts)}
# for cat_path, cat_data in sorted(norm_data.items()):
# cat_name = cat_data['name'].replace('/', '-')
# lines.append(f"{cat_data['id']}:{cat_path}:{cat_name}")
# print(f'Catalog writen at: {catalog_path}')
# catalog_path.write_text('\n'.join(lines), encoding="utf-8")
@ -321,27 +328,27 @@ class LibraryType(PropertyGroup):
# return write_file(cache_path, list(asset_infos))
def prop_rel_path(self, path, prop):
'''Get a filepath relative to a property of the library_type'''
field_prop = '{%s}/'%prop
"""Get a filepath relative to a property of the library_type"""
field_prop = "{%s}/" % prop
prop_value = getattr(self, prop)
prop_value = Path(os.path.expandvars(prop_value)).resolve()
rel_path = Path(path).resolve().relative_to(prop_value).as_posix()
return field_prop + rel_path
def format_from_ext(self, ext):
if ext.startswith('.'):
if ext.startswith("."):
ext = ext[1:]
file_format = ext.upper()
if file_format == 'JPG':
file_format = 'JPEG'
elif file_format == 'EXR':
file_format = 'OPEN_EXR'
if file_format == "JPG":
file_format = "JPEG"
elif file_format == "EXR":
file_format = "OPEN_EXR"
return file_format
def save_image(self, image, filepath, remove=False):
@ -368,47 +375,54 @@ class LibraryType(PropertyGroup):
filepath = Path(filepath)
filepath.parent.mkdir(parents=True, exist_ok=True)
img_size = preview.image_size
px = [0] * img_size[0] * img_size[1] * 4
preview.image_pixels_float.foreach_get(px)
img = bpy.data.images.new(name=filepath.name, width=img_size[0], height=img_size[1], is_data=True, alpha=True)
img = bpy.data.images.new(
name=filepath.name,
width=img_size[0],
height=img_size[1],
is_data=True,
alpha=True,
)
img.pixels.foreach_set(px)
self.save_image(img, filepath, remove=True)
def draw_header(self, layout):
"""Draw the header of the Asset Browser Window"""
#layout.separator()
# layout.separator()
self.module_type.gui.draw_header(layout)
def draw_context_menu(self, layout):
"""Draw the context menu of the Asset Browser Window"""
"""Draw the context menu of the Asset Browser Window"""
self.module_type.gui.draw_context_menu(layout)
def generate_blend_preview(self, asset_info):
asset_name = asset_info['name']
catalog = asset_info['catalog']
asset_name = asset_info["name"]
catalog = asset_info["catalog"]
asset_path = self.format_path(asset_info['filepath'])
asset_path = self.format_path(asset_info["filepath"])
dst_image_path = self.get_image_path(asset_name, asset_path, catalog)
if dst_image_path.exists():
return
# Check if a source image exists and if so copying it in the new directory
src_image_path = asset_info.get('image')
src_image_path = asset_info.get("image")
if src_image_path:
src_image_path = self.get_template_path(src_image_path, asset_name, asset_path, catalog)
src_image_path = self.get_template_path(
src_image_path, asset_name, asset_path, catalog
)
if src_image_path and src_image_path.exists():
self.copy_file(src_image_path, dst_image_path)
return
print(f'Thumbnailing {asset_path} to {dst_image_path}')
blender_thumbnailer = Path(bpy.app.binary_path).parent / 'blender-thumbnailer'
print(f"Thumbnailing {asset_path} to {dst_image_path}")
blender_thumbnailer = Path(bpy.app.binary_path).parent / "blender-thumbnailer"
dst_image_path.parent.mkdir(exist_ok=True, parents=True)
@ -417,7 +431,7 @@ class LibraryType(PropertyGroup):
success = dst_image_path.exists()
if not success:
empty_preview = RESOURCES_DIR / 'empty_preview.png'
empty_preview = RESOURCES_DIR / "empty_preview.png"
self.copy_file(str(empty_preview), str(dst_image_path))
return success
@ -532,14 +546,12 @@ class LibraryType(PropertyGroup):
# def set_asset_catalog(self, asset, asset_data, catalog_data):
# """Find the catalog if already exist or create it"""
# catalog_name = asset_data['catalog']
# catalog = catalog_data.get(catalog_name)
# catalog_item = self.catalog.add(asset_data['catalog'])
# asset.asset_data.catalog_id = catalog_item.id
# if not catalog:
# catalog = {'id': str(uuid.uuid4()), 'name': catalog_name}
# catalog_data[catalog_name] = catalog
@ -547,7 +559,7 @@ class LibraryType(PropertyGroup):
# asset.asset_data.catalog_id = catalog['id']
def set_asset_metadata(self, asset, asset_cache):
"""Create custom prop to an asset base on provided data"""
"""Create custom prop to an asset base on provided data"""
for k, v in asset_cache.metadata.items():
asset.asset_data[k] = v
@ -570,18 +582,22 @@ class LibraryType(PropertyGroup):
"""Get the bundle path for that asset"""
catalog_parts = asset_cache.catalog_item.parts
blend_name = asset_cache.norm_name
path_parts = catalog_parts[:self.library.blend_depth]
path_parts = catalog_parts[: self.library.blend_depth]
return Path(self.bundle_directory, *path_parts, blend_name, blend_name).with_suffix('.blend')
return Path(
self.bundle_directory, *path_parts, blend_name, blend_name
).with_suffix(".blend")
def bundle(self, cache_diff=None):
"""Group all new assets in one or multiple blends for the asset browser"""
supported_types = ('FILE', 'ACTION', 'COLLECTION')
supported_operations = ('ADD', 'REMOVE', 'MODIFY')
supported_types = ("FILE", "ACTION", "COLLECTION")
supported_operations = ("ADD", "REMOVE", "MODIFY")
if self.data_type not in supported_types:
print(f'{self.data_type} is not supported yet supported types are {supported_types}')
print(
f"{self.data_type} is not supported yet supported types are {supported_types}"
)
return
catalog = self.read_catalog()
@ -595,92 +611,99 @@ class LibraryType(PropertyGroup):
# Write the cache in a temporary file for the generate preview script
tmp_cache_file = cache.write(tmp=True)
bpy.ops.assetlib.generate_previews(name=self.library.name, cache=str(tmp_cache_file))
bpy.ops.assetlib.generate_previews(
name=self.library.name, cache=str(tmp_cache_file)
)
elif isinstance(cache_diff, (Path, str)):
cache_diff = LibraryCacheDiff(cache_diff).read()#json.loads(Path(cache_diff).read_text(encoding='utf-8'))
cache_diff = LibraryCacheDiff(
cache_diff
).read() # json.loads(Path(cache_diff).read_text(encoding='utf-8'))
total_diffs = len(cache_diff)
print(f'Total Diffs={total_diffs}')
print(f"Total Diffs={total_diffs}")
if total_diffs == 0:
print('No assets found')
print("No assets found")
return
i = 0
for bundle_path, asset_diffs in cache_diff.group_by(self.get_asset_bundle_path):
if bundle_path.exists():
print(f'Opening existing bundle blend: {bundle_path}')
print(f"Opening existing bundle blend: {bundle_path}")
bpy.ops.wm.open_mainfile(filepath=str(bundle_path))
else:
print(f'Create new bundle blend to: {bundle_path}')
print(f"Create new bundle blend to: {bundle_path}")
bpy.ops.wm.read_homefile(use_empty=True)
for asset_diff in asset_diffs:
if total_diffs <= 100 or i % int(total_diffs / 10) == 0:
print(f'Progress: {int(i / total_diffs * 100)+1}')
print(f"Progress: {int(i / total_diffs * 100)+1}")
operation = asset_diff.operation
asset_cache = asset_diff.asset_cache
asset = getattr(bpy.data, self.data_types).get(asset_cache.name)
if operation == 'REMOVE':
if operation == "REMOVE":
if asset:
getattr(bpy.data, self.data_types).remove(asset)
else:
print(f'ERROR : Remove Asset: {asset_cache.name} not found in {bundle_path}')
print(
f"ERROR : Remove Asset: {asset_cache.name} not found in {bundle_path}"
)
continue
elif operation == 'MODIFY':
elif operation == "MODIFY":
if not asset:
print(f'WARNING: Modifiy Asset: {asset_cache.name} not found in {bundle_path} it will be created')
print(
f"WARNING: Modifiy Asset: {asset_cache.name} not found in {bundle_path} it will be created"
)
if operation == 'ADD' or not asset:
if operation == "ADD" or not asset:
if asset:
#raise Exception(f"Asset {asset_data['name']} Already in Blend")
# raise Exception(f"Asset {asset_data['name']} Already in Blend")
print(f"Asset {asset_cache.name} Already in Blend")
getattr(bpy.data, self.data_types).remove(asset)
#print(f"INFO: Add new asset: {asset_data['name']}")
asset = getattr(bpy.data, self.data_types).new(name=asset_cache.name)
# print(f"INFO: Add new asset: {asset_data['name']}")
asset = getattr(bpy.data, self.data_types).new(
name=asset_cache.name
)
asset.asset_mark()
self.set_asset_preview(asset, asset_cache)
#if not asset_preview:
# if not asset_preview:
# assets_to_preview.append((asset_data['filepath'], asset_data['name'], asset_data['data_type']))
#if self.externalize_data:
# if self.externalize_data:
# self.write_preview(preview, filepath)
#self.set_asset_catalog(asset, asset_data['catalog'])
# self.set_asset_catalog(asset, asset_data['catalog'])
asset.asset_data.catalog_id = catalog.add(asset_cache.catalog).id
self.set_asset_metadata(asset, asset_cache)
self.set_asset_tags(asset, asset_cache)
self.set_asset_info(asset, asset_cache)
i += 1
#self.write_asset_preview_file()
# self.write_asset_preview_file()
print(f'Saving Blend to {bundle_path}')
print(f"Saving Blend to {bundle_path}")
bundle_path.parent.mkdir(exist_ok=True, parents=True)
bpy.ops.wm.save_as_mainfile(filepath=str(bundle_path), compress=True)
if write_cache:
cache.write()
#self.write_catalog(catalog_data)
# self.write_catalog(catalog_data)
catalog.write()
bpy.ops.wm.quit_blender()
# def unflatten_cache(self, cache):
# """ Return a new unflattten list of asset data
# grouped by filepath"""
@ -725,7 +748,7 @@ class LibraryType(PropertyGroup):
# new_cache.append({**asset_info, **asset_data})
# else:
# new_cache.append(asset_info)
# return new_cache
# def diff(self, asset_infos=None):
@ -736,7 +759,7 @@ class LibraryType(PropertyGroup):
# if cache is None:
# print(f'Fetch The library {self.library.name} for the first time, might be long...')
# cache = []
# asset_infos = asset_infos or self.fetch()
# cache = {f"{a['filepath']}/{a['name']}": a for a in self.flatten_cache(cache)}
@ -752,7 +775,7 @@ class LibraryType(PropertyGroup):
# print(f'{len(assets_removed)} Assets Removed \n{tuple(a["name"] for a in assets_removed[:10])}\n')
# if assets_modified:
# print(f'{len(assets_modified)} Assets Modified \n{tuple(a["name"] for a in assets_modified[:10])}\n')
# assets_added = [dict(a, operation='ADD') for a in assets_added]
# assets_removed = [dict(a, operation='REMOVE') for a in assets_removed]
# assets_modified = [dict(a, operation='MODIFY') for a in assets_modified]
@ -769,4 +792,3 @@ class LibraryType(PropertyGroup):
annotations = self.__class__.__annotations__
for k, v in annotations.items():
layout.prop(self, k, text=bpy.path.display_name(k))

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,27 +25,34 @@ from pprint import pprint as pp
REQ_HEADERS = requests.utils.default_headers()
REQ_HEADERS.update({"User-Agent": "Blender: PH Assets"})
class PolyHaven(LibraryType):
name = "Poly Haven"
# template_name : StringProperty()
# template_file : StringProperty()
directory : StringProperty(subtype='DIR_PATH')
asset_type : EnumProperty(items=[(i.replace(' ', '_').upper(), i, '') for i in ('HDRIs', 'Models', 'Textures')], default='HDRIS')
main_category : StringProperty(
default='artificial light, natural light, nature, studio, skies, urban'
directory: StringProperty(subtype="DIR_PATH")
asset_type: EnumProperty(
items=[
(i.replace(" ", "_").upper(), i, "")
for i in ("HDRIs", "Models", "Textures")
],
default="HDRIS",
)
secondary_category : StringProperty(
default='high constrast, low constrast, medium constrast, midday, morning-afternoon, night, sunrise-sunset'
main_category: StringProperty(
default="artificial light, natural light, nature, studio, skies, urban"
)
secondary_category: StringProperty(
default="high constrast, low constrast, medium constrast, midday, morning-afternoon, night, sunrise-sunset"
)
#blend_depth: IntProperty(default=1)
# blend_depth: IntProperty(default=1)
# url: StringProperty()
# login: StringProperty()
# password: StringProperty(subtype='PASSWORD')
# project_name: StringProperty()
def get_asset_path(self, name, catalog, directory=None):
# chemin: Source, Asset_type, asset_name / asset_name.blend -> PolyHaven/HDRIs/test/test.blend
directory = directory or self.source_directory
@ -64,37 +69,40 @@ class PolyHaven(LibraryType):
def format_asset_info(self, asset_info, asset_path):
# prend un asset info et output un asset description
asset_path = self.prop_rel_path(asset_path, 'source_directory')
modified = asset_info.get('modified', time.time_ns())
asset_path = self.prop_rel_path(asset_path, "source_directory")
modified = asset_info.get("modified", time.time_ns())
return dict(
filepath=asset_path,
modified=modified,
library_id=self.library.id,
assets=[dict(
catalog=asset_data.get('catalog', asset_info['catalog']),
author=asset_data.get('author'),
metadata=asset_data.get('metadata', {}),
description=asset_data.get('description', ''),
tags=asset_data.get('tags', []),
type=self.data_type,
image=self.template_image,
video=self.template_video,
name=asset_data['name']) for asset_data in asset_info['assets']
]
assets=[
dict(
catalog=asset_data.get("catalog", asset_info["catalog"]),
author=asset_data.get("author"),
metadata=asset_data.get("metadata", {}),
description=asset_data.get("description", ""),
tags=asset_data.get("tags", []),
type=self.data_type,
image=self.template_image,
video=self.template_video,
name=asset_data["name"],
)
for asset_data in asset_info["assets"]
],
)
def fetch(self):
"""Gather in a list all assets found in the folder"""
print(f'Fetch Assets for {self.library.name}')
print('self.asset_type: ', self.asset_type)
print(f"Fetch Assets for {self.library.name}")
print("self.asset_type: ", self.asset_type)
url = f"https://api.polyhaven.com/assets?t={self.asset_type.lower()}"
# url2 = f"https://polyhaven.com/{self.asset_type.lower()}"
# url += "&future=true" if early_access else ""
# verify_ssl = not bpy.context.preferences.addons["polyhavenassets"].preferences.disable_ssl_verify
verify_ssl = False
try:
res = requests.get(url, headers=REQ_HEADERS, verify=verify_ssl)
@ -108,34 +116,33 @@ class PolyHaven(LibraryType):
error = f"Error retrieving asset list, status code: {res.status_code}"
print(error)
# return (error, None)
catalog = None
# return (None, res.json())
for asset_info in res.json().values():
main_category = None
secondary_category = None
for category in asset_info['categories']:
for category in asset_info["categories"]:
if category in self.main_category and not main_category:
main_category = category
if category in self.secondary_category and not secondary_category:
secondary_category = category
if main_category and secondary_category:
catalog = f'{main_category}_{secondary_category}'
catalog = f"{main_category}_{secondary_category}"
if not catalog:
return
asset_path = self.get_asset_path(asset_info['name'], catalog)
print('asset_path: ', asset_path)
asset_path = self.get_asset_path(asset_info["name"], catalog)
print("asset_path: ", asset_path)
asset_info = self.format_asset_info(asset_info, asset_path)
print('asset_info: ', asset_info)
print("asset_info: ", asset_info)
# return self.format_asset_info([asset['name'], self.get_asset_path(asset['name'], catalog) for asset, asset_infos in res.json().items()])
# return self.format_asset_info([asset['name'], self.get_asset_path(asset['name'], catalog) for asset, asset_infos in res.json().items()])
# pp(res.json())
# pp(res2.json())
# print(res2)
# return asset_infos

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,21 +21,21 @@ import time
class ScanFolder(LibraryType):
name = "Scan Folder"
source_directory : StringProperty(subtype='DIR_PATH')
source_directory: StringProperty(subtype="DIR_PATH")
source_template_file : StringProperty()
source_template_image : StringProperty()
source_template_video : StringProperty()
source_template_info : StringProperty()
source_template_file: StringProperty()
source_template_image: StringProperty()
source_template_video: StringProperty()
source_template_info: StringProperty()
def draw_prefs(self, layout):
layout.prop(self, "source_directory", text="Source: Directory")
col = layout.column(align=True)
col.prop(self, "source_template_file", icon='COPY_ID', text='Template file')
col.prop(self, "source_template_image", icon='COPY_ID', text='Template image')
col.prop(self, "source_template_video", icon='COPY_ID', text='Template video')
col.prop(self, "source_template_info", icon='COPY_ID', text='Template info')
col.prop(self, "source_template_file", icon="COPY_ID", text="Template file")
col.prop(self, "source_template_image", icon="COPY_ID", text="Template image")
col.prop(self, "source_template_video", icon="COPY_ID", text="Template video")
col.prop(self, "source_template_info", icon="COPY_ID", text="Template info")
def get_asset_path(self, name, catalog, directory=None):
directory = directory or self.source_directory
@ -49,20 +47,26 @@ class ScanFolder(LibraryType):
def get_image_path(self, name, catalog, filepath):
catalog = self.norm_file_name(catalog)
name = self.norm_file_name(name)
return self.format_path(self.source_template_image, dict(name=name, catalog=catalog, filepath=filepath))
return self.format_path(
self.source_template_image,
dict(name=name, catalog=catalog, filepath=filepath),
)
def get_video_path(self, name, catalog, filepath):
catalog = self.norm_file_name(catalog)
name = self.norm_file_name(name)
return self.format_path(self.source_template_video, dict(name=name, catalog=catalog, filepath=filepath))
return self.format_path(
self.source_template_video,
dict(name=name, catalog=catalog, filepath=filepath),
)
def new_asset(self, asset, asset_data):
raise Exception('Need to be defined in the library_type')
raise Exception("Need to be defined in the library_type")
def remove_asset(self, asset, asset_data):
raise Exception('Need to be defined in the library_type')
raise Exception("Need to be defined in the library_type")
'''
"""
def format_asset_info(self, asset_datas, asset_path, modified=None):
asset_path = self.prop_rel_path(asset_path, 'source_directory')
@ -97,38 +101,38 @@ class ScanFolder(LibraryType):
name=asset_data['name']) for asset_data in asset_datas
]
)
'''
"""
def set_asset_preview(self, asset, asset_cache):
'''Load an externalize image as preview for an asset using the source template'''
"""Load an externalize image as preview for an asset using the source template"""
asset_path = self.format_path(asset_cache.filepath)
image_template = self.source_template_image
if not image_template:
return
image_path = self.find_path(image_template, asset_cache.to_dict(), filepath=asset_path)
image_path = self.find_path(
image_template, asset_cache.to_dict(), filepath=asset_path
)
if image_path:
with bpy.context.temp_override(id=asset):
bpy.ops.ed.lib_id_load_custom_preview(
filepath=str(image_path)
)
bpy.ops.ed.lib_id_load_custom_preview(filepath=str(image_path))
else:
print(f'No image found for {image_template} on {asset.name}')
print(f"No image found for {image_template} on {asset.name}")
if asset.preview:
return asset.preview
def bundle(self, cache_diff=None):
"""Group all new assets in one or multiple blends for the asset browser"""
if self.data_type not in ('FILE', 'ACTION', 'COLLECTION'):
print(f'{self.data_type} is not supported yet')
if self.data_type not in ("FILE", "ACTION", "COLLECTION"):
print(f"{self.data_type} is not supported yet")
return
#catalog_data = self.read_catalog()
# catalog_data = self.read_catalog()
catalog = self.read_catalog()
cache = None
@ -140,59 +144,69 @@ class ScanFolder(LibraryType):
# Write the cache in a temporary file for the generate preview script
tmp_cache_file = cache.write(tmp=True)
bpy.ops.assetlib.generate_previews(name=self.library.name, cache=str(tmp_cache_file))
bpy.ops.assetlib.generate_previews(
name=self.library.name, cache=str(tmp_cache_file)
)
elif isinstance(cache_diff, (Path, str)):
cache_diff = json.loads(Path(cache_diff).read_text(encoding='utf-8'))
cache_diff = json.loads(Path(cache_diff).read_text(encoding="utf-8"))
if self.library.blend_depth == 0:
raise Exception('Blender depth must be 1 at min')
raise Exception("Blender depth must be 1 at min")
total_assets = len(cache_diff)
print(f'total_assets={total_assets}')
print(f"total_assets={total_assets}")
if total_assets == 0:
print('No assets found')
print("No assets found")
return
i = 0
for blend_path, asset_cache_diffs in cache_diff.group_by(key=self.get_asset_bundle_path):
for blend_path, asset_cache_diffs in cache_diff.group_by(
key=self.get_asset_bundle_path
):
if blend_path.exists():
print(f'Opening existing bundle blend: {blend_path}')
print(f"Opening existing bundle blend: {blend_path}")
bpy.ops.wm.open_mainfile(filepath=str(blend_path))
else:
print(f'Create new bundle blend to: {blend_path}')
print(f"Create new bundle blend to: {blend_path}")
bpy.ops.wm.read_homefile(use_empty=True)
for asset_cache_diff in asset_cache_diffs:
if total_assets <= 100 or i % int(total_assets / 10) == 0:
print(f'Progress: {int(i / total_assets * 100)+1}')
print(f"Progress: {int(i / total_assets * 100)+1}")
operation = asset_cache_diff.operation
asset_cache = asset_cache_diff.asset_cache
asset_name = asset_cache.name
asset = getattr(bpy.data, self.data_types).get(asset_name)
if operation == 'REMOVE':
if operation == "REMOVE":
if asset:
getattr(bpy.data, self.data_types).remove(asset)
else:
print(f'ERROR : Remove Asset: {asset_name} not found in {blend_path}')
print(
f"ERROR : Remove Asset: {asset_name} not found in {blend_path}"
)
continue
if asset_cache_diff.operation == 'MODIFY' and not asset:
print(f'WARNING: Modifiy Asset: {asset_name} not found in {blend_path} it will be created')
if asset_cache_diff.operation == "MODIFY" and not asset:
print(
f"WARNING: Modifiy Asset: {asset_name} not found in {blend_path} it will be created"
)
if operation == 'ADD' or not asset:
if operation == "ADD" or not asset:
if asset:
#raise Exception(f"Asset {asset_name} Already in Blend")
# raise Exception(f"Asset {asset_name} Already in Blend")
print(f"Asset {asset_name} Already in Blend")
getattr(bpy.data, self.data_types).remove(asset)
#print(f"INFO: Add new asset: {asset_name}")
# print(f"INFO: Add new asset: {asset_name}")
asset = getattr(bpy.data, self.data_types).new(name=asset_name)
else:
print(f'operation {operation} not supported should be in (ADD, REMOVE, MODIFY)')
print(
f"operation {operation} not supported should be in (ADD, REMOVE, MODIFY)"
)
continue
asset.asset_mark()
@ -202,14 +216,13 @@ class ScanFolder(LibraryType):
self.set_asset_metadata(asset, asset_cache)
self.set_asset_tags(asset, asset_cache)
self.set_asset_info(asset, asset_cache)
i += 1
print(f'Saving Blend to {blend_path}')
print(f"Saving Blend to {blend_path}")
blend_path.parent.mkdir(exist_ok=True, parents=True)
bpy.ops.wm.save_as_mainfile(filepath=str(blend_path), compress=True)
# If the variable cache_diff was given we need to update the cache with the diff
if cache is None:
@ -220,82 +233,84 @@ class ScanFolder(LibraryType):
catalog.update(cache.catalogs)
catalog.write()
bpy.ops.wm.quit_blender()
def fetch(self):
"""Gather in a list all assets found in the folder"""
print(f'Fetch Assets for {self.library.name}')
print(f"Fetch Assets for {self.library.name}")
source_directory = Path(self.source_directory)
template_file = Template(self.source_template_file)
#catalog_data = self.read_catalog(directory=source_directory)
#catalog_ids = {v['id']: k for k, v in catalog_data.items()}
# catalog_data = self.read_catalog(directory=source_directory)
# catalog_ids = {v['id']: k for k, v in catalog_data.items()}
#self.catalog.read()
# self.catalog.read()
cache = self.read_cache()
print(f'Search for blend using glob template: {template_file.glob_pattern}')
print(f'Scanning Folder {source_directory}...')
print(f"Search for blend using glob template: {template_file.glob_pattern}")
print(f"Scanning Folder {source_directory}...")
#new_cache = LibraryCache()
# new_cache = LibraryCache()
for asset_path in template_file.glob(source_directory):
source_rel_path = self.prop_rel_path(asset_path, 'source_directory')
source_rel_path = self.prop_rel_path(asset_path, "source_directory")
modified = asset_path.stat().st_mtime_ns
# Check if the asset description as already been cached
file_cache = next((a for a in cache if a.filepath == source_rel_path), None)
if file_cache:
if file_cache.modified >= modified: #print(asset_path, 'is skipped because not modified')
if (
file_cache.modified >= modified
): # print(asset_path, 'is skipped because not modified')
continue
else:
file_cache = cache.add(filepath=source_rel_path)
rel_path = asset_path.relative_to(source_directory).as_posix()
field_data = template_file.parse(rel_path)
# Create the catalog path from the actual path of the asset
catalog = [v for k,v in sorted(field_data.items()) if re.findall('cat[0-9]+', k)]
#catalogs = [c.replace('_', ' ').title() for c in catalogs]
catalog = [
v for k, v in sorted(field_data.items()) if re.findall("cat[0-9]+", k)
]
# catalogs = [c.replace('_', ' ').title() for c in catalogs]
asset_name = field_data.get('asset_name', asset_path.stem)
asset_name = field_data.get("asset_name", asset_path.stem)
if self.data_type == 'FILE':
if self.data_type == "FILE":
file_cache.set_data(
name=asset_name,
type='FILE',
catalog=catalog,
modified=modified
name=asset_name, type="FILE", catalog=catalog, modified=modified
)
continue
# Now check if there is a asset description file (Commented for now propably not usefull)
#asset_info_path = self.find_path(self.source_template_info, asset_info, filepath=asset_path)
#if asset_info_path:
# asset_info_path = self.find_path(self.source_template_info, asset_info, filepath=asset_path)
# if asset_info_path:
# new_cache.append(self.read_file(asset_info_path))
# continue
# Scan the blend file for assets inside
print(f'Scanning blendfile {asset_path}...')
assets = self.load_datablocks(asset_path, type=self.data_types, link=True, assets_only=True)
print(f'Found {len(assets)} {self.data_types} inside')
print(f"Scanning blendfile {asset_path}...")
assets = self.load_datablocks(
asset_path, type=self.data_types, link=True, assets_only=True
)
print(f"Found {len(assets)} {self.data_types} inside")
for asset in assets:
#catalog_path = catalog_ids.get(asset.asset_data.catalog_id)
# catalog_path = catalog_ids.get(asset.asset_data.catalog_id)
#if not catalog_path:
# if not catalog_path:
# print(f'No catalog found for asset {asset.name}')
#catalog_path = asset_info['catalog']#asset_path.relative_to(self.source_directory).as_posix()
# catalog_path = asset_info['catalog']#asset_path.relative_to(self.source_directory).as_posix()
# For now the catalog used is the one extract from the template file
file_cache.assets.add(self.get_asset_data(asset), catalog=catalog)
getattr(bpy.data, self.data_types).remove(asset)
return cache

View File

@ -1,7 +1,6 @@
from typing import Set
#import shutil
# import shutil
from pathlib import Path
import subprocess
import importlib
@ -11,22 +10,19 @@ import json
import bpy
from bpy_extras import asset_utils
from bpy.types import Context, Operator
from bpy.props import (
BoolProperty,
EnumProperty,
StringProperty,
IntProperty)
from bpy.props import BoolProperty, EnumProperty, StringProperty, IntProperty
#from asset_library.constants import (DATA_TYPES, DATA_TYPE_ITEMS, MODULE_DIR)
# from asset_library.constants import (DATA_TYPES, DATA_TYPE_ITEMS, MODULE_DIR)
import asset_library
from asset_library.common.bl_utils import (
attr_set,
get_addon_prefs,
get_bl_cmd,
get_addon_prefs,
get_bl_cmd,
get_view3d_persp,
#suitable_areas,
# suitable_areas,
refresh_asset_browsers,
load_datablocks)
load_datablocks,
)
from asset_library.common.file_utils import open_blender_file, synchronize
from asset_library.common.functions import get_active_library, asset_warning_callback
@ -42,8 +38,8 @@ import bgl
class ASSETLIB_OT_remove_assets(Operator):
bl_idname = "assetlib.remove_assets"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
bl_label = 'Remove Assets'
bl_description = 'Remove Selected Assets'
bl_label = "Remove Assets"
bl_description = "Remove Selected Assets"
@classmethod
def poll(cls, context):
@ -51,29 +47,33 @@ class ASSETLIB_OT_remove_assets(Operator):
return False
sp = context.space_data
if sp.params.asset_library_ref == 'LOCAL':
if sp.params.asset_library_ref == "LOCAL":
return False
return True
def execute(self, context: Context) -> Set[str]:
asset = context.active_file
lib = get_active_library()
lib_type = lib.library_type
catalog = lib.read_catalog()
if not catalog.context.item:
self.report({'ERROR'}, 'The active asset is not in the catalog')
return {'CANCELLED'}
self.report({"ERROR"}, "The active asset is not in the catalog")
return {"CANCELLED"}
asset_name = context.asset_file_handle.name
asset_path = lib_type.format_path(asset.asset_data['filepath'])
asset_path = lib_type.format_path(asset.asset_data["filepath"])
asset_catalog = catalog.context.path
img_path = lib_type.get_image_path(name=asset_name, catalog=asset_catalog, filepath=asset_path)
video_path = lib_type.get_video_path(name=asset_name, catalog=asset_catalog, filepath=asset_path)
img_path = lib_type.get_image_path(
name=asset_name, catalog=asset_catalog, filepath=asset_path
)
video_path = lib_type.get_video_path(
name=asset_name, catalog=asset_catalog, filepath=asset_path
)
if asset_path and asset_path.exists():
asset_path.unlink()
@ -81,16 +81,16 @@ class ASSETLIB_OT_remove_assets(Operator):
img_path.unlink()
if video_path and video_path.exists():
video_path.unlink()
#open_blender_file(filepath)
# open_blender_file(filepath)
try:
asset_path.parent.rmdir()
except Exception:#Directory not empty
except Exception: # Directory not empty
pass
bpy.ops.assetlib.bundle(name=lib.name, blocking=True)
return {'FINISHED'}
return {"FINISHED"}
class ASSETLIB_OT_edit_data(Operator):
@ -99,12 +99,18 @@ class ASSETLIB_OT_edit_data(Operator):
bl_description = "Edit Current Asset Data"
bl_options = {"REGISTER", "UNDO"}
warning: StringProperty(name='')
path: StringProperty(name='Path')
catalog: StringProperty(name='Catalog', update=asset_warning_callback, options={'TEXTEDIT_UPDATE'})
name: StringProperty(name='Name', update=asset_warning_callback, options={'TEXTEDIT_UPDATE'})
tags: StringProperty(name='Tags', description='Tags need to separate with a comma (,)')
description: StringProperty(name='Description')
warning: StringProperty(name="")
path: StringProperty(name="Path")
catalog: StringProperty(
name="Catalog", update=asset_warning_callback, options={"TEXTEDIT_UPDATE"}
)
name: StringProperty(
name="Name", update=asset_warning_callback, options={"TEXTEDIT_UPDATE"}
)
tags: StringProperty(
name="Tags", description="Tags need to separate with a comma (,)"
)
description: StringProperty(name="Description")
@classmethod
def poll(cls, context):
@ -120,15 +126,17 @@ class ASSETLIB_OT_edit_data(Operator):
lib = prefs.libraries[lib.store_library]
new_name = lib.library_type.norm_file_name(self.name)
new_asset_path = lib.library_type.get_asset_path(name=new_name, catalog=self.catalog)
new_asset_path = lib.library_type.get_asset_path(
name=new_name, catalog=self.catalog
)
#asset_data = lib.library_type.get_asset_data(self.asset)
# asset_data = lib.library_type.get_asset_data(self.asset)
asset_data = dict(
tags=[t.strip() for t in self.tags.split(',') if t],
tags=[t.strip() for t in self.tags.split(",") if t],
description=self.description,
)
#lib.library_type.set_asset_catalog(asset, asset_data, catalog_data)
# lib.library_type.set_asset_catalog(asset, asset_data, catalog_data)
self.asset.name = self.name
lib.library_type.set_asset_tags(self.asset, asset_data)
lib.library_type.set_asset_info(self.asset, asset_data)
@ -137,37 +145,51 @@ class ASSETLIB_OT_edit_data(Operator):
lib.library_type.write_asset(asset=self.asset, asset_path=new_asset_path)
if self.old_image_path.exists():
new_img_path = lib.library_type.get_image_path(new_name, self.catalog, new_asset_path)
new_img_path = lib.library_type.get_image_path(
new_name, self.catalog, new_asset_path
)
self.old_image_path.rename(new_img_path)
if self.old_video_path.exists():
new_video_path = lib.library_type.get_video_path(new_name, self.catalog, new_asset_path)
new_video_path = lib.library_type.get_video_path(
new_name, self.catalog, new_asset_path
)
self.old_video_path.rename(new_video_path)
#if self.old_description_path.exists():
# if self.old_description_path.exists():
# self.old_description_path.unlink()
try:
self.old_asset_path.parent.rmdir()
except Exception: #The folder is not empty
except Exception: # The folder is not empty
pass
diff_path = Path(bpy.app.tempdir, 'diff.json')
diff = [dict(name=self.old_asset_name, catalog=self.old_catalog, filepath=str(self.old_asset_path), operation='REMOVE')]
diff_path = Path(bpy.app.tempdir, "diff.json")
diff = [
dict(
name=self.old_asset_name,
catalog=self.old_catalog,
filepath=str(self.old_asset_path),
operation="REMOVE",
)
]
asset_data = lib.library_type.get_asset_data(self.asset)
diff += [dict(asset_data,
image=str(new_img_path),
filepath=str(new_asset_path),
type=lib.data_type,
library_id=lib.id,
catalog=self.catalog,
operation='ADD'
)]
diff += [
dict(
asset_data,
image=str(new_img_path),
filepath=str(new_asset_path),
type=lib.data_type,
library_id=lib.id,
catalog=self.catalog,
operation="ADD",
)
]
print(diff)
diff_path.write_text(json.dumps(diff, indent=4), encoding='utf-8')
diff_path.write_text(json.dumps(diff, indent=4), encoding="utf-8")
bpy.ops.assetlib.bundle(name=lib.name, diff=str(diff_path), blocking=True)
@ -182,25 +204,25 @@ class ASSETLIB_OT_edit_data(Operator):
lib = get_active_library()
if lib.merge_libraries:
layout.prop(lib, 'store_library', expand=False)
layout.prop(lib, "store_library", expand=False)
layout.prop(self, "catalog", text="Catalog")
layout.prop(self, "name", text="Name")
layout.prop(self, 'tags')
layout.prop(self, 'description')
layout.prop(self, "tags")
layout.prop(self, "description")
#layout.prop()
# layout.prop()
layout.separator()
col = layout.column()
col.use_property_split = False
#row.enabled = False
# row.enabled = False
if self.path:
col.label(text=self.path)
if self.warning:
col.label(icon='ERROR', text=self.warning)
col.label(icon="ERROR", text=self.warning)
def invoke(self, context, event):
@ -213,70 +235,74 @@ class ASSETLIB_OT_edit_data(Operator):
asset_handle = context.asset_file_handle
catalog_file = lib.library_type.read_catalog()
catalog_ids = {v['id']: {'path': k, 'name': v['name']} for k,v in catalog_file.items()}
catalog_ids = {
v["id"]: {"path": k, "name": v["name"]} for k, v in catalog_file.items()
}
#asset_handle = context.asset_file_handle
# asset_handle = context.asset_file_handle
self.old_asset_name = asset_handle.name
self.old_asset_path = lib.library_type.get_active_asset_path()
self.asset = load_datablocks(self.old_asset_path, self.old_asset_name, type=lib.data_types)
self.asset = load_datablocks(
self.old_asset_path, self.old_asset_name, type=lib.data_types
)
if not self.asset:
self.report({'ERROR'}, 'No asset found')
self.report({"ERROR"}, "No asset found")
self.name = self.old_asset_name
self.description = asset_handle.asset_data.description
tags = [t.strip() for t in self.asset.asset_data.tags.keys() if t]
self.tags = ', '.join(tags)
#asset_path
self.old_catalog = catalog_ids[asset_handle.asset_data.catalog_id]['path']
self.tags = ", ".join(tags)
# asset_path
self.old_catalog = catalog_ids[asset_handle.asset_data.catalog_id]["path"]
self.catalog = self.old_catalog
self.old_image_path = lib.library_type.get_image_path(name=self.name, catalog=self.catalog, filepath=self.old_asset_path)
self.old_video_path = lib.library_type.get_video_path(name=self.name, catalog=self.catalog, filepath=self.old_asset_path)
#self.old_description_path = lib.library_type.get_description_path(self.old_asset_path)
#self.old_asset_info = lib.library_type.read_asset_info_file(self.old_asset_path)
#self.old_asset_info = lib.library_type.norm_asset_datas([self.old_asset_info])[0]
self.old_image_path = lib.library_type.get_image_path(
name=self.name, catalog=self.catalog, filepath=self.old_asset_path
)
self.old_video_path = lib.library_type.get_video_path(
name=self.name, catalog=self.catalog, filepath=self.old_asset_path
)
# self.old_description_path = lib.library_type.get_description_path(self.old_asset_path)
# self.old_asset_info = lib.library_type.read_asset_info_file(self.old_asset_path)
# self.old_asset_info = lib.library_type.norm_asset_datas([self.old_asset_info])[0]
return context.window_manager.invoke_props_dialog(self, width=450)
def cancel(self, context):
print('Cancel Edit Data, removing the asset')
print("Cancel Edit Data, removing the asset")
lib = get_active_library()
active_lib = lib.library_type.get_active_asset_library()
getattr(bpy.data, active_lib.data_types).remove(self.asset)
class ASSETLIB_OT_remove_user_library(Operator):
bl_idname = "assetlib.remove_user_library"
bl_options = {"REGISTER", "UNDO"}
bl_label = 'Remove User Library'
bl_description = 'Remove User Library'
bl_label = "Remove User Library"
bl_description = "Remove User Library"
index : IntProperty(default=-1)
index: IntProperty(default=-1)
def execute(self, context: Context) -> Set[str]:
prefs = get_addon_prefs()
prefs.user_libraries.remove(self.index)
return {'FINISHED'}
return {"FINISHED"}
class ASSETLIB_OT_add_user_library(Operator):
bl_idname = "assetlib.add_user_library"
bl_options = {"REGISTER", "UNDO"}
bl_label = 'Add User Library'
bl_description = 'Add User Library'
bl_label = "Add User Library"
bl_description = "Add User Library"
def execute(self, context: Context) -> Set[str]:
prefs = get_addon_prefs()
@ -284,72 +310,77 @@ class ASSETLIB_OT_add_user_library(Operator):
lib = prefs.user_libraries.add()
lib.expand = True
return {'FINISHED'}
return {"FINISHED"}
class ASSETLIB_OT_open_blend(Operator):
bl_idname = "assetlib.open_blend"
bl_options = {"REGISTER", "UNDO"}
bl_label = 'Open Blender File'
bl_description = 'Open blender file'
bl_label = "Open Blender File"
bl_description = "Open blender file"
#filepath : StringProperty(subtype='FILE_PATH')
# filepath : StringProperty(subtype='FILE_PATH')
def execute(self, context: Context) -> Set[str]:
#asset = context.active_file
#prefs = get_addon_prefs()
# asset = context.active_file
# prefs = get_addon_prefs()
lib = get_active_library()
#filepath = lib.library_type.format_path(asset.asset_data['filepath'])
# filepath = lib.library_type.format_path(asset.asset_data['filepath'])
filepath = lib.library_type.get_active_asset_path()
open_blender_file(filepath)
return {'FINISHED'}
return {"FINISHED"}
class ASSETLIB_OT_set_paths(Operator):
bl_idname = "assetlib.set_paths"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
bl_label = 'Set Paths'
bl_description = 'Set Library Paths'
bl_label = "Set Paths"
bl_description = "Set Library Paths"
name: StringProperty()
all: BoolProperty(default=False)
def execute(self, context: Context) -> Set[str]:
prefs = get_addon_prefs()
print('Set Paths')
print("Set Paths")
if self.all:
libs = prefs.libraries
else:
libs = [prefs.libraries[self.name]]
for lib in libs:
lib.clear_library_path()
lib.set_library_path()
return {'FINISHED'}
return {"FINISHED"}
class ASSETLIB_OT_bundle_library(Operator):
bl_idname = "assetlib.bundle"
bl_options = {"INTERNAL"}
bl_label = 'Bundle Library'
bl_description = 'Bundle all matching asset found inside one blend'
bl_label = "Bundle Library"
bl_description = "Bundle all matching asset found inside one blend"
name : StringProperty()
diff : StringProperty()
blocking : BoolProperty(default=False)
mode : EnumProperty(items=[(i.replace(' ', '_').upper(), i, '') for i in ('None', 'All', 'Auto Bundle')], default='NONE')
directory : StringProperty(subtype='DIR_PATH')
#conform : BoolProperty(default=False)
#def refresh(self):
name: StringProperty()
diff: StringProperty()
blocking: BoolProperty(default=False)
mode: EnumProperty(
items=[
(i.replace(" ", "_").upper(), i, "") for i in ("None", "All", "Auto Bundle")
],
default="NONE",
)
directory: StringProperty(subtype="DIR_PATH")
# conform : BoolProperty(default=False)
# def refresh(self):
# for area in suitable_areas(bpy.context.screen):
# bpy.ops.asset.library_refresh({"area": area, 'region': area.regions[3]})
#space_data.activate_asset_by_id(asset, deferred=deferred)
# space_data.activate_asset_by_id(asset, deferred=deferred)
def execute(self, context: Context) -> Set[str]:
prefs = get_addon_prefs()
@ -358,19 +389,20 @@ class ASSETLIB_OT_bundle_library(Operator):
if self.name:
libs += [prefs.libraries[self.name]]
if self.mode == 'ALL':
if self.mode == "ALL":
libs += prefs.libraries.values()
elif self.mode == 'AUTO_BUNDLE':
elif self.mode == "AUTO_BUNDLE":
libs += [l for l in prefs.libraries if l.auto_bundle]
if not libs:
return {"CANCELLED"}
lib_datas = [l.to_dict() for l in libs]
print(f'Bundle Libraries: {[l.name for l in libs]}')
print(f"Bundle Libraries: {[l.name for l in libs]}")
script_code = dedent(f"""
script_code = dedent(
f"""
import bpy
prefs = bpy.context.preferences.addons["asset_library"].preferences
@ -380,52 +412,53 @@ class ASSETLIB_OT_bundle_library(Operator):
lib.library_type.bundle(cache_diff='{self.diff}')
bpy.ops.wm.quit_blender()
""")
"""
)
script_path = Path(bpy.app.tempdir) / 'bundle_library.py'
script_path = Path(bpy.app.tempdir) / "bundle_library.py"
script_path.write_text(script_code)
print(script_code)
#raise Exception()
# raise Exception()
cmd = get_bl_cmd(script=str(script_path), background=True)
#print(cmd)
# print(cmd)
if self.blocking:
subprocess.call(cmd)
bpy.app.timers.register(refresh_asset_browsers, first_interval=0.2)
else:
subprocess.Popen(cmd)
return {'FINISHED'}
return {"FINISHED"}
class ASSETLIB_OT_reload_addon(Operator):
bl_idname = "assetlib.reload_addon"
bl_options = {"UNDO"}
bl_label = 'Reload Asset Library Addon'
bl_description = 'Reload The Asset Library Addon and the addapters'
bl_label = "Reload Asset Library Addon"
bl_description = "Reload The Asset Library Addon and the addapters"
def execute(self, context: Context) -> Set[str]:
print('Execute reload')
print("Execute reload")
asset_library.unregister()
importlib.reload(asset_library)
asset_library.register()
return {'FINISHED'}
return {"FINISHED"}
class ASSETLIB_OT_diff(Operator):
bl_idname = "assetlib.diff"
bl_options = {"REGISTER", "UNDO"}
bl_label = 'Synchronize'
bl_description = 'Synchronize Action Lib to Local Directory'
bl_label = "Synchronize"
bl_description = "Synchronize Action Lib to Local Directory"
name : StringProperty()
conform : BoolProperty(default=False)
name: StringProperty()
conform: BoolProperty(default=False)
def execute(self, context: Context) -> Set[str]:
prefs = get_addon_prefs()
@ -433,7 +466,8 @@ class ASSETLIB_OT_diff(Operator):
lib = prefs.libraries.get(self.name)
lib.library_type.diff()
return {'FINISHED'}
return {"FINISHED"}
'''
class ASSETLIB_OT_conform_library(Operator):
@ -482,24 +516,25 @@ class ASSETLIB_OT_conform_library(Operator):
return {'RUNNING_MODAL'}
'''
class ASSETLIB_OT_make_custom_preview(Operator):
bl_idname = "assetlib.make_custom_preview"
bl_label = "Custom Preview"
bl_description = "Set a camera to preview an asset"
image_size : IntProperty(default=512)
modal : BoolProperty(default=False)
image_size: IntProperty(default=512)
modal: BoolProperty(default=False)
def modal(self, context, event):
if event.type in {'ESC'}: # Cancel
if event.type in {"ESC"}: # Cancel
self.restore()
return {'CANCELLED'}
return {"CANCELLED"}
elif event.type in {'RET', 'NUMPAD_ENTER'}: # Cancel
elif event.type in {"RET", "NUMPAD_ENTER"}: # Cancel
return self.execute(context)
#return {'FINISHED'}
# return {'FINISHED'}
return {'PASS_THROUGH'}
return {"PASS_THROUGH"}
def execute(self, context):
@ -508,14 +543,13 @@ class ASSETLIB_OT_make_custom_preview(Operator):
img_path = context.scene.render.filepath
#print('Load Image to previews')
prefs.previews.load(Path(img_path).stem, img_path, 'IMAGE')
#img = bpy.data.images.load(context.scene.render.filepath)
#img.update()
#img.preview_ensure()
# print('Load Image to previews')
prefs.previews.load(Path(img_path).stem, img_path, "IMAGE")
# img = bpy.data.images.load(context.scene.render.filepath)
# img.update()
# img.preview_ensure()
#Copy the image with a new name
# Copy the image with a new name
# render = bpy.data.images['Render Result']
# render_pixels = [0] * self.image_size * self.image_size * 4
@ -523,32 +557,30 @@ class ASSETLIB_OT_make_custom_preview(Operator):
# img = bpy.data.images.new(name=img_name, width=self.image_size, height=self.image_size, is_data=True, alpha=True)
# img.pixels.foreach_set(render_pixels)
#img.scale(128, 128)
#img.preview_ensure()
# img.scale(128, 128)
# img.preview_ensure()
# preview_size = render.size
# pixels = [0] * preview_size[0] * preview_size[1] * 4
# render.pixels.foreach_get(pixels)
# image.preview.image_size = preview_size
# image.preview.image_pixels_float.foreach_set(pixels)
self.restore()
#self.is_running = False
# self.is_running = False
prefs.preview_modal = False
return {"FINISHED"}
def restore(self):
print('RESTORE')
print("RESTORE")
try:
bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW')
bpy.types.SpaceView3D.draw_handler_remove(self._handle, "WINDOW")
except:
print('Failed remove handler')
print("Failed remove handler")
pass
bpy.data.objects.remove(self.camera)
@ -562,27 +594,27 @@ class ASSETLIB_OT_make_custom_preview(Operator):
bg_color = (0.8, 0.1, 0.1, 0.5)
font_color = (1, 1, 1, 1)
text = f'Escape: Cancel Enter: Make Preview'
text = f"Escape: Cancel Enter: Make Preview"
font_id = 0
dim = blf.dimensions(font_id, text)
#gpu.state.line_width_set(100)
# gpu.state.line_width_set(100)
# bgl.glLineWidth(100)
# self.shader_2d.bind()
# self.shader_2d.uniform_float("color", bg_color)
# self.screen_framing.draw(self.shader_2d)
# # Reset
# gpu.state.line_width_set(1)
# -dim[0]/2, +dim[1]/2 + 5
# Display Text
blf.color(font_id, *font_color) # unpack color
blf.position(font_id, context.region.width/2 -dim[0]/2, dim[1]/2 + 5, 0)
blf.color(font_id, *font_color) # unpack color
blf.position(font_id, context.region.width / 2 - dim[0] / 2, dim[1] / 2 + 5, 0)
blf.size(font_id, 12, dpi)
blf.draw(font_id, f'Escape: Cancel Enter: Make Preview')
blf.draw(font_id, f"Escape: Cancel Enter: Make Preview")
def get_image_name(self):
prefs = get_addon_prefs()
preview_names = [p for p in prefs.previews.keys()]
@ -592,45 +624,47 @@ class ASSETLIB_OT_make_custom_preview(Operator):
if preview_names:
index = int(preview_names[-1][-2:]) + 1
return f'preview_{index:03d}'
return f"preview_{index:03d}"
def invoke(self, context, event):
prefs = get_addon_prefs()
cam_data = bpy.data.cameras.new(name='Preview Camera')
self.camera = bpy.data.objects.new(name='Preview Camera', object_data=cam_data)
cam_data = bpy.data.cameras.new(name="Preview Camera")
self.camera = bpy.data.objects.new(name="Preview Camera", object_data=cam_data)
#view_3d = get_view3d_persp()
# view_3d = get_view3d_persp()
scn = context.scene
space = context.space_data
matrix = space.region_3d.view_matrix.inverted()
if space.region_3d.view_perspective == 'CAMERA':
if space.region_3d.view_perspective == "CAMERA":
matrix = scn.camera.matrix_world
self.camera.matrix_world = matrix
img_name = self.get_image_name()
img_path = Path(bpy.app.tempdir, img_name).with_suffix('.webp')
img_path = Path(bpy.app.tempdir, img_name).with_suffix(".webp")
self.attr_changed = attr_set([
(space.overlay, 'show_overlays', False),
(space.region_3d, 'view_perspective', 'CAMERA'),
(space.region_3d, 'view_camera_offset'),
(space.region_3d, 'view_camera_zoom'),
(space, 'lock_camera', True),
(space, 'show_region_ui', False),
(scn, 'camera', self.camera),
(scn.render, 'resolution_percentage', 100),
(scn.render, 'resolution_x', self.image_size),
(scn.render, 'resolution_y', self.image_size),
(scn.render, 'film_transparent', True),
(scn.render.image_settings, 'file_format', 'WEBP'),
(scn.render.image_settings, 'color_mode', 'RGBA'),
#(scn.render.image_settings, 'color_depth', '8'),
(scn.render, 'use_overwrite', True),
(scn.render, 'filepath', str(img_path)),
])
self.attr_changed = attr_set(
[
(space.overlay, "show_overlays", False),
(space.region_3d, "view_perspective", "CAMERA"),
(space.region_3d, "view_camera_offset"),
(space.region_3d, "view_camera_zoom"),
(space, "lock_camera", True),
(space, "show_region_ui", False),
(scn, "camera", self.camera),
(scn.render, "resolution_percentage", 100),
(scn.render, "resolution_x", self.image_size),
(scn.render, "resolution_y", self.image_size),
(scn.render, "film_transparent", True),
(scn.render.image_settings, "file_format", "WEBP"),
(scn.render.image_settings, "color_mode", "RGBA"),
# (scn.render.image_settings, 'color_depth', '8'),
(scn.render, "use_overwrite", True),
(scn.render, "filepath", str(img_path)),
]
)
bpy.ops.view3d.view_center_camera()
space.region_3d.view_camera_zoom -= 6
@ -643,14 +677,17 @@ class ASSETLIB_OT_make_custom_preview(Operator):
if self.modal:
prefs.preview_modal = True
self.shader_2d = gpu.shader.from_builtin('2D_UNIFORM_COLOR')
self.shader_2d = gpu.shader.from_builtin("2D_UNIFORM_COLOR")
self.screen_framing = batch_for_shader(
self.shader_2d, 'LINE_LOOP', {"pos": [(0,0), (0,h), (w,h), (w,0)]})
self.shader_2d, "LINE_LOOP", {"pos": [(0, 0), (0, h), (w, h), (w, 0)]}
)
self._handle = bpy.types.SpaceView3D.draw_handler_add(self.draw_callback_px, (context,), 'WINDOW', 'POST_PIXEL')
self._handle = bpy.types.SpaceView3D.draw_handler_add(
self.draw_callback_px, (context,), "WINDOW", "POST_PIXEL"
)
context.window_manager.modal_handler_add(self)
return {'RUNNING_MODAL'}
return {"RUNNING_MODAL"}
else:
return self.execute(context)
@ -661,10 +698,10 @@ class ASSETLIB_OT_generate_previews(Operator):
bl_label = "Generate Previews"
bl_description = "Generate and write the image for assets"
cache : StringProperty()
preview_blend : StringProperty()
name : StringProperty()
blocking : BoolProperty(default=True)
cache: StringProperty()
preview_blend: StringProperty()
name: StringProperty()
blocking: BoolProperty(default=True)
def execute(self, context: Context) -> Set[str]:
prefs = get_addon_prefs()
@ -678,17 +715,18 @@ class ASSETLIB_OT_generate_previews(Operator):
# cmd = [
# bpy.app.binary_path, '-b', '--use-system-env',
# '--python', str(PREVIEW_ASSETS_SCRIPT), '--',
# '--preview-blend', str(self.preview_blend),
# '--preview-blend', str(self.preview_blend),
# '--preview-assets-file', str(self.preview_assets_file)
# ]
# subprocess.call(cmd)
preview_blend = self.preview_blend or lib.library_type.preview_blend
if not preview_blend or not Path(preview_blend).exists():
preview_blend = MODULE_DIR / 'common' / 'preview.blend'
script_path = Path(bpy.app.tempdir) / 'generate_previews.py'
script_code = dedent(f"""
if not preview_blend or not Path(preview_blend).exists():
preview_blend = MODULE_DIR / "common" / "preview.blend"
script_path = Path(bpy.app.tempdir) / "generate_previews.py"
script_code = dedent(
f"""
import bpy
prefs = bpy.context.preferences.addons["asset_library"].preferences
lib = prefs.env_libraries.add()
@ -696,7 +734,8 @@ class ASSETLIB_OT_generate_previews(Operator):
bpy.ops.wm.open_mainfile(filepath='{preview_blend}', load_ui=True)
lib.library_type.generate_previews(cache='{self.cache}')
""")
"""
)
script_path.write_text(script_code)
@ -707,15 +746,14 @@ class ASSETLIB_OT_generate_previews(Operator):
else:
subprocess.Popen(cmd)
return {'FINISHED'}
return {"FINISHED"}
class ASSETLIB_OT_play_preview(Operator):
bl_idname = "assetlib.play_preview"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
bl_label = 'Play Preview'
bl_description = 'Play Preview'
bl_label = "Play Preview"
bl_description = "Play Preview"
@classmethod
def poll(cls, context: Context) -> bool:
@ -738,64 +776,63 @@ class ASSETLIB_OT_play_preview(Operator):
lib = get_active_library()
#filepath = lib.library_type.format_path(asset.asset_data['filepath'])
# filepath = lib.library_type.format_path(asset.asset_data['filepath'])
asset_path = lib.library_type.get_active_asset_path()
asset_image = lib.library_type.get_image(asset.name, asset_path)
asset_video = lib.library_type.get_video(asset.name, asset_path)
if not asset_image and not asset_video:
self.report({'ERROR'}, f'Preview for {asset.name} not found.')
self.report({"ERROR"}, f"Preview for {asset.name} not found.")
return {"CANCELLED"}
if asset_video:
self.report({'INFO'}, f'Video found. {asset_video}.')
self.report({"INFO"}, f"Video found. {asset_video}.")
if prefs.video_player:
subprocess.Popen([prefs.video_player, asset_video])
else:
bpy.ops.wm.path_open(filepath=str(asset_video))
else:
self.report({'INFO'}, f'Image found. {asset_image}.')
self.report({"INFO"}, f"Image found. {asset_image}.")
if prefs.image_player:
subprocess.Popen([prefs.image_player, asset_image])
else:
bpy.ops.wm.path_open(filepath=str(asset_image))
return {"FINISHED"}
class ASSETLIB_OT_synchronize(Operator):
bl_idname = "assetlib.synchronize"
bl_options = {"REGISTER", "UNDO"}
bl_label = 'Synchronize'
bl_description = 'Synchronize Action Lib to Local Directory'
bl_label = "Synchronize"
bl_description = "Synchronize Action Lib to Local Directory"
clean : BoolProperty(default=False)
only_new : BoolProperty(default=False)
only_recent : BoolProperty(default=False)
clean: BoolProperty(default=False)
only_new: BoolProperty(default=False)
only_recent: BoolProperty(default=False)
name: StringProperty()
all: BoolProperty(default=False)
def execute(self, context: Context) -> Set[str]:
print('Not yet Implemented, have to be replace by Bundle instead')
return {'FINISHED'}
print("Not yet Implemented, have to be replace by Bundle instead")
return {"FINISHED"}
prefs = get_addon_prefs()
print('Synchronize')
print("Synchronize")
if self.all:
libs = prefs.libraries
else:
libs = [prefs.libraries.get(self.name)]
for lib in libs:
for lib in libs:
if self.clean and Path(lib.path_local).exists():
pass
print('To check first')
#shutil.rmtree(path_local)
print("To check first")
# shutil.rmtree(path_local)
if not lib.path_local:
continue
@ -804,10 +841,11 @@ class ASSETLIB_OT_synchronize(Operator):
src=lib.path,
dst=lib.path_local,
only_new=self.only_new,
only_recent=self.only_recent
only_recent=self.only_recent,
)
return {'FINISHED'}
return {"FINISHED"}
classes = (
ASSETLIB_OT_play_preview,
@ -821,17 +859,19 @@ classes = (
ASSETLIB_OT_bundle_library,
ASSETLIB_OT_remove_assets,
ASSETLIB_OT_edit_data,
#ASSETLIB_OT_conform_library,
# ASSETLIB_OT_conform_library,
ASSETLIB_OT_reload_addon,
ASSETLIB_OT_make_custom_preview
ASSETLIB_OT_make_custom_preview,
)
def register():
#bpy.types.UserAssetLibrary.is_env = False
# bpy.types.UserAssetLibrary.is_env = False
for cls in classes:
bpy.utils.register_class(cls)
def unregister():
for cls in reversed(classes):
bpy.utils.unregister_class(cls)
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()
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,25 +61,28 @@ class POSELIB_OT_create_pose_asset(Operator):
pose_name: StringProperty(name="Pose Name") # type: ignore
activate_new_action: BoolProperty(name="Activate New Action", default=True) # type: ignore
@classmethod
def poll(cls, context: Context) -> bool:
# Make sure that if there is an asset browser open, the artist can see the newly created pose asset.
asset_browse_area: Optional[bpy.types.Area] = asset_browser.area_from_context(context)
asset_browse_area: Optional[bpy.types.Area] = asset_browser.area_from_context(
context
)
if not asset_browse_area:
# No asset browser is visible, so there also aren't any expectations
# that this asset will be visible.
return True
asset_space_params = asset_browser.params(asset_browse_area)
if asset_space_params.asset_library_ref != 'LOCAL':
cls.poll_message_set("Asset Browser must be set to the Current File library")
if asset_space_params.asset_library_ref != "LOCAL":
cls.poll_message_set(
"Asset Browser must be set to the Current File library"
)
return False
return True
def execute(self, context: Context) -> Set[str]:
#pose_name = self.pose_name or context.object.name
# pose_name = self.pose_name or context.object.name
pose_name = False
if context.object.animation_data:
if context.object.animation_data.action:
@ -85,28 +90,28 @@ class POSELIB_OT_create_pose_asset(Operator):
if pose_name:
prefix = True
asset_name = Path(bpy.data.filepath).stem.split('_')[0]
asset_name = Path(bpy.data.filepath).stem.split("_")[0]
action_asset_name = re.search(f'^{asset_name}.', pose_name)
action_asset_name = re.search(f"^{asset_name}.", pose_name)
if action_asset_name:
pose_name = pose_name.replace(action_asset_name.group(0), '')
pose_name = pose_name.replace(action_asset_name.group(0), "")
side = re.search('_\w$', pose_name)
side = re.search("_\w$", pose_name)
if side:
pose_name = pose_name.replace(side.group(0), '')
pose_name = pose_name.replace(side.group(0), "")
if 'hands' in context.object.animation_data.action.name.lower():
pose_name = f'hand_{pose_name}'
if pose_name.startswith('lips_'):
pose_name.replace('lips_', '')
split = pose_name.split('_')
pose_name = '-'.join([s for s in split if s.isupper()])
pose_name = f'{pose_name}_{split[-1]}'
if "hands" in context.object.animation_data.action.name.lower():
pose_name = f"hand_{pose_name}"
if pose_name.startswith("lips_"):
pose_name.replace("lips_", "")
split = pose_name.split("_")
pose_name = "-".join([s for s in split if s.isupper()])
pose_name = f"{pose_name}_{split[-1]}"
prefix = False
if prefix and not pose_name.startswith(asset_name):
pose_name = f'{asset_name}_{pose_name}'
pose_name = f"{asset_name}_{pose_name}"
else:
pose_name = self.pose_name or context.object.name
@ -126,7 +131,6 @@ class POSELIB_OT_create_pose_asset(Operator):
if context.scene.camera:
data_dict.update(dict(camera=context.scene.camera.name))
for k, v in data_dict.items():
data[k] = v
###
@ -134,7 +138,7 @@ class POSELIB_OT_create_pose_asset(Operator):
if self.activate_new_action:
self._set_active_action(context, asset)
self._activate_asset_in_browser(context, asset)
return {'FINISHED'}
return {"FINISHED"}
def _set_active_action(self, context: Context, asset: Action) -> None:
self._prevent_action_loss(context.object)
@ -149,7 +153,9 @@ class POSELIB_OT_create_pose_asset(Operator):
This makes it possible to immediately check & edit the created pose asset.
"""
asset_browse_area: Optional[bpy.types.Area] = asset_browser.area_from_context(context)
asset_browse_area: Optional[bpy.types.Area] = asset_browser.area_from_context(
context
)
if not asset_browse_area:
return
@ -181,7 +187,9 @@ class POSELIB_OT_create_pose_asset(Operator):
return
action.use_fake_user = True
self.report({'WARNING'}, "Action %s marked Fake User to prevent loss" % action.name)
self.report(
{"WARNING"}, "Action %s marked Fake User to prevent loss" % action.name
)
class POSELIB_OT_restore_previous_action(Operator):
@ -215,17 +223,17 @@ class POSELIB_OT_restore_previous_action(Operator):
self._timer = wm.event_timer_add(0.001, window=context.window)
wm.modal_handler_add(self)
return {'RUNNING_MODAL'}
return {"RUNNING_MODAL"}
def modal(self, context, event):
if event.type != 'TIMER':
return {'RUNNING_MODAL'}
if event.type != "TIMER":
return {"RUNNING_MODAL"}
wm = context.window_manager
wm.event_timer_remove(self._timer)
context.object.pose.apply_pose_from_action(self.pose_action)
return {'FINISHED'}
return {"FINISHED"}
class ASSET_OT_assign_action(Operator):
@ -257,7 +265,9 @@ class ASSET_OT_assign_action(Operator):
class POSELIB_OT_copy_as_asset(Operator):
bl_idname = "poselib.copy_as_asset"
bl_label = "Copy Pose As Asset"
bl_description = "Create a new pose asset on the clipboard, to be pasted into an Asset Browser"
bl_description = (
"Create a new pose asset on the clipboard, to be pasted into an Asset Browser"
)
bl_options = {"REGISTER"}
CLIPBOARD_ASSET_MARKER = "ASSET-BLEND="
@ -289,7 +299,10 @@ class POSELIB_OT_copy_as_asset(Operator):
filepath,
)
asset_browser.tag_redraw(context.screen)
self.report({"INFO"}, "Pose Asset copied, use Paste As New Asset in any Asset Browser to paste")
self.report(
{"INFO"},
"Pose Asset copied, use Paste As New Asset in any Asset Browser to paste",
)
# The asset has been saved to disk, so to clean up it has to loose its asset & fake user status.
asset.asset_clear()
@ -300,7 +313,10 @@ class POSELIB_OT_copy_as_asset(Operator):
if asset.users > 0:
# This should never happen, and indicates a bug in the code. Having a warning about it is nice,
# but it shouldn't stand in the way of actually cleaning up the meant-to-be-temporary datablock.
self.report({"WARNING"}, "Unexpected non-zero user count for the asset, please report this as a bug")
self.report(
{"WARNING"},
"Unexpected non-zero user count for the asset, please report this as a bug",
)
bpy.data.actions.remove(asset)
return {"FINISHED"}
@ -331,8 +347,10 @@ class POSELIB_OT_paste_asset(Operator):
return False
asset_lib_ref = context.space_data.params.asset_library_ref
if asset_lib_ref != 'LOCAL':
cls.poll_message_set("Asset Browser must be set to the Current File library")
if asset_lib_ref != "LOCAL":
cls.poll_message_set(
"Asset Browser must be set to the Current File library"
)
return False
# Delay checking the clipboard as much as possible, as it's CPU-heavier than the other checks.
@ -348,7 +366,6 @@ class POSELIB_OT_paste_asset(Operator):
return True
def execute(self, context: Context) -> Set[str]:
clipboard = context.window_manager.clipboard
marker_len = len(POSELIB_OT_copy_as_asset.CLIPBOARD_ASSET_MARKER)
@ -379,18 +396,18 @@ class POSELIB_OT_paste_asset(Operator):
class POSELIB_OT_pose_asset_select_bones(Operator):
bl_idname = "poselib.pose_asset_select_bones"
bl_label = "Select Bones"
#bl_description = "Select those bones that are used in this pose"
# bl_description = "Select those bones that are used in this pose"
bl_description = "Click: Select used Bones\nAlt+Click: Select Flipped Bones\nCtrl+Click: Select Both sides."
bl_options = {"REGISTER", "UNDO"}
#bl_property = "selected_side"
# bl_property = "selected_side"
selected_side: EnumProperty(
name='Selected Side',
name="Selected Side",
items=(
('CURRENT', "Current", ""),
('FLIPPED', "Flipped", ""),
('BOTH', "Both", ""),
)
("CURRENT", "Current", ""),
("FLIPPED", "Flipped", ""),
("BOTH", "Both", ""),
),
)
@classmethod
@ -402,7 +419,7 @@ class POSELIB_OT_pose_asset_select_bones(Operator):
and context.asset_file_handle
):
return False
return context.asset_file_handle.id_type == 'ACTION'
return context.asset_file_handle.id_type == "ACTION"
def execute(self, context: Context) -> Set[str]:
asset: FileSelectEntry = context.asset_file_handle
@ -417,7 +434,9 @@ class POSELIB_OT_pose_asset_select_bones(Operator):
def _load_and_use_pose(self, context: Context) -> Set[str]:
asset_library_ref = context.asset_library_ref
asset = context.asset_file_handle
asset_lib_path = bpy.types.AssetHandle.get_full_library_path(asset, asset_library_ref)
asset_lib_path = bpy.types.AssetHandle.get_full_library_path(
asset, asset_library_ref
)
if not asset_lib_path:
self.report( # type: ignore
@ -426,7 +445,7 @@ class POSELIB_OT_pose_asset_select_bones(Operator):
f"Selected asset {asset.name} could not be located inside the asset library",
)
return {"CANCELLED"}
if asset.id_type != 'ACTION':
if asset.id_type != "ACTION":
self.report( # type: ignore
{"ERROR"},
f"Selected asset {asset.name} is not an Action",
@ -442,10 +461,13 @@ class POSELIB_OT_pose_asset_select_bones(Operator):
def use_pose(self, context: Context, pose_asset: Action) -> Set[str]:
arm_object: Object = context.object
#pose_usage.select_bones(arm_object, pose_asset, select=self.select, flipped=self.flipped)
pose_usage.select_bones(arm_object, pose_asset, selected_side=self.selected_side)
# pose_usage.select_bones(arm_object, pose_asset, select=self.select, flipped=self.flipped)
pose_usage.select_bones(
arm_object, pose_asset, selected_side=self.selected_side
)
return {"FINISHED"}
# This operator takes the Window Manager's `actionlib_flipped` property, and
# passes it to the `POSELIB_OT_blend_pose_asset` operator. This makes it
# possible to bind a key to the operator and still have it respect the global
@ -464,7 +486,7 @@ class POSELIB_OT_blend_pose_asset_for_keymap(Operator):
@classmethod
def poll(cls, context: Context) -> bool:
return bpy.ops.poselib.blend_pose_asset.poll(context.copy())
"""
def invoke(self, context, event):
if event.type == 'LEFTMOUSE':
@ -478,10 +500,14 @@ class POSELIB_OT_blend_pose_asset_for_keymap(Operator):
"""
def invoke(self, context: Context, event: Event) -> Set[str]:
return bpy.ops.poselib.blend_pose_asset(context.copy(), 'INVOKE_DEFAULT', flipped=self.flipped)
return bpy.ops.poselib.blend_pose_asset(
context.copy(), "INVOKE_DEFAULT", flipped=self.flipped
)
def execute(self, context: Context) -> Set[str]:
return bpy.ops.poselib.blend_pose_asset(context.copy(), 'EXEC_DEFAULT', flipped=self.flipped)
return bpy.ops.poselib.blend_pose_asset(
context.copy(), "EXEC_DEFAULT", flipped=self.flipped
)
# This operator takes the Window Manager's `actionlib_flipped` property, and
@ -489,14 +515,15 @@ class POSELIB_OT_blend_pose_asset_for_keymap(Operator):
# possible to bind a key to the operator and still have it respect the global
# "Flip Pose" checkbox.
class POSELIB_OT_apply_pose_asset_for_keymap(Operator):
bl_idname = "poselib.apply_pose_asset_for_keymap"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
_rna = bpy.ops.poselib.apply_pose_asset.get_rna_type()
bl_label = _rna.name
#bl_description = _rna.description
bl_description = 'Apply Pose to Bones'
# bl_description = _rna.description
bl_description = "Apply Pose to Bones"
del _rna
flipped: BoolProperty(name="Flipped", default=False) # type: ignore
@ -506,27 +533,42 @@ class POSELIB_OT_apply_pose_asset_for_keymap(Operator):
if not asset_utils.SpaceAssetInfo.is_asset_browser(context.space_data):
return False
return bpy.ops.poselib.apply_pose_asset.poll(context.copy())
def execute(self, context: Context) -> Set[str]:
if self.flipped:
action = bpy.data.actions.get(context.active_file.name)
store_bones = {}
bones = [
'blendshape-eyes', 'blendshape-eye.L', 'blendshape-eye.R',
'blendshape-corner-mouth', 'blendshape-corner-mouth.L',
'blendshape-corner-down-mouth.L', 'blendshape-corner-up-mouth.L',
'blendshape-corner-mouth-add.L','blendshape-corner-mouth.R',
'blendshape-corner-down-mouth.R', 'blendshape-corner-up-mouth.R',
'blendshape-corner-mouth-add.R', 'blendshape-center-up-mouth',
'blendshape-center-down-mouth',
'hat1.R', 'hat2.R', 'hat3.R', 'hat1.L', 'hat2.L', 'hat3.L',
"blendshape-eyes",
"blendshape-eye.L",
"blendshape-eye.R",
"blendshape-corner-mouth",
"blendshape-corner-mouth.L",
"blendshape-corner-down-mouth.L",
"blendshape-corner-up-mouth.L",
"blendshape-corner-mouth-add.L",
"blendshape-corner-mouth.R",
"blendshape-corner-down-mouth.R",
"blendshape-corner-up-mouth.R",
"blendshape-corner-mouth-add.R",
"blendshape-center-up-mouth",
"blendshape-center-down-mouth",
"hat1.R",
"hat2.R",
"hat3.R",
"hat1.L",
"hat2.L",
"hat3.L",
]
attributes = [
'location', 'rotation_quaternion',
'rotation_euler', 'rotation_axis_angle', 'scale'
"location",
"rotation_quaternion",
"rotation_euler",
"rotation_axis_angle",
"scale",
]
if action:
@ -534,7 +576,7 @@ class POSELIB_OT_apply_pose_asset_for_keymap(Operator):
bone_name, prop_name = split_path(fc.data_path)
if bone_name not in bones:
continue
if not bone_name in store_bones.keys():
store_bones[bone_name] = {}
@ -543,30 +585,40 @@ class POSELIB_OT_apply_pose_asset_for_keymap(Operator):
if not prop_name in store_bones[bone_name].keys():
store_bones[bone_name][prop_name] = []
val = getattr(context.object.pose.bones[bone_name], prop_name)
val = getattr(
context.object.pose.bones[bone_name], prop_name
)
store_bones[bone_name][prop_name].append(fc.evaluate(context.scene.frame_current))
bpy.ops.poselib.apply_pose_asset(context.copy(), 'EXEC_DEFAULT', flipped=True)
store_bones[bone_name][prop_name].append(
fc.evaluate(context.scene.frame_current)
)
bpy.ops.poselib.apply_pose_asset(
context.copy(), "EXEC_DEFAULT", flipped=True
)
for bone, v in store_bones.items():
for attr, attr_val in v.items():
flipped_vector = 1
### TODO FAIRE ÇA PROPREMENT AVEC UNE COMPREHENSION LIST OU AUTRE
if re.search(r'\.[RL]$', bone):
if re.search(r"\.[RL]$", bone):
flipped_bone = pose_usage.flip_side_name(bone)
if attr == 'location':
if attr == "location":
flipped_vector = Vector((-1, 1, 1))
# print('-----', store_bones.get(flipped_bone)[attr])
attr_val = Vector(store_bones.get(flipped_bone)[attr]) * flipped_vector
attr_val = (
Vector(store_bones.get(flipped_bone)[attr]) * flipped_vector
)
setattr(context.object.pose.bones[bone], attr, attr_val)
return {'FINISHED'}
return {"FINISHED"}
else:
return bpy.ops.poselib.apply_pose_asset(context.copy(), 'EXEC_DEFAULT', flipped=False)
return bpy.ops.poselib.apply_pose_asset(
context.copy(), "EXEC_DEFAULT", flipped=False
)
class POSELIB_OT_convert_old_poselib(Operator):
@ -577,12 +629,18 @@ class POSELIB_OT_convert_old_poselib(Operator):
@classmethod
def poll(cls, context: Context) -> bool:
action = context.object and context.object.animation_data and context.object.animation_data.action
action = (
context.object
and context.object.animation_data
and context.object.animation_data.action
)
if not action:
cls.poll_message_set("Active object has no Action")
return False
if not action.pose_markers:
cls.poll_message_set("Action %r is not a old-style pose library" % action.name)
cls.poll_message_set(
"Action %r is not a old-style pose library" % action.name
)
return False
return True
@ -593,12 +651,11 @@ class POSELIB_OT_convert_old_poselib(Operator):
new_actions = conversion.convert_old_poselib(old_poselib)
if not new_actions:
self.report({'ERROR'}, "Unable to convert to pose assets")
return {'CANCELLED'}
self.report({'INFO'}, "Converted %d poses to pose assets" % len(new_actions))
return {'FINISHED'}
self.report({"ERROR"}, "Unable to convert to pose assets")
return {"CANCELLED"}
self.report({"INFO"}, "Converted %d poses to pose assets" % len(new_actions))
return {"FINISHED"}
classes = (
@ -609,7 +666,7 @@ classes = (
POSELIB_OT_create_pose_asset,
POSELIB_OT_paste_asset,
POSELIB_OT_pose_asset_select_bones,
POSELIB_OT_restore_previous_action
POSELIB_OT_restore_previous_action,
)
register, unregister = bpy.utils.register_classes_factory(classes)

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,10 +300,13 @@ def create_pose_asset(
pose_action.asset_generate_preview()
return pose_action
#def create_pose_asset_from_context(context: Context, new_asset_name: str, selection=True) -> Optional[Action]:
def create_pose_asset_from_context(context: Context, new_asset_name: str) -> Optional[Action]:
# def create_pose_asset_from_context(context: Context, new_asset_name: str, selection=True) -> Optional[Action]:
def create_pose_asset_from_context(
context: Context, new_asset_name: str
) -> Optional[Action]:
"""Create Action asset from active object & selected bones."""
bones = context.selected_pose_bones_from_active_object
bone_names = {bone.name for bone in bones}
@ -369,7 +376,10 @@ def copy_keyframe(dst_fcurve: FCurve, src_keyframe: Keyframe) -> Keyframe:
"""Copy a keyframe from one FCurve to the other."""
dst_keyframe = dst_fcurve.keyframe_points.insert(
src_keyframe.co.x, src_keyframe.co.y, options={'FAST'}, keyframe_type=src_keyframe.type
src_keyframe.co.x,
src_keyframe.co.y,
options={"FAST"},
keyframe_type=src_keyframe.type,
)
for propname in {
@ -412,7 +422,9 @@ def find_keyframe(fcurve: FCurve, frame: float) -> Optional[Keyframe]:
return None
def assign_from_asset_browser(asset: Action, asset_browser_area: bpy.types.Area) -> None:
def assign_from_asset_browser(
asset: Action, asset_browser_area: bpy.types.Area
) -> None:
"""Assign some things from the asset browser to the asset.
This sets the current catalog ID, and in the future could include tags

View File

@ -14,7 +14,7 @@ from bpy.types import (
)
#def select_bones(arm_object: Object, action: Action, *, select: bool, flipped: bool, both=False) -> None:
# def select_bones(arm_object: Object, action: Action, *, select: bool, flipped: bool, both=False) -> None:
def select_bones(arm_object: Object, action: Action, *, selected_side, toggle=True):
pose_bone_re = re.compile(r'pose.bones\["([^"]+)"\]')
pose = arm_object.pose
@ -34,15 +34,14 @@ def select_bones(arm_object: Object, action: Action, *, selected_side, toggle=Tr
if bone_name in seen_bone_names:
continue
seen_bone_names.add(bone_name)
if selected_side == 'FLIPPED':
if selected_side == "FLIPPED":
bones_to_select.add(bone_name_flip)
elif selected_side == 'BOTH':
elif selected_side == "BOTH":
bones_to_select.add(bone_name_flip)
bones_to_select.add(bone_name)
elif selected_side == 'CURRENT':
elif selected_side == "CURRENT":
bones_to_select.add(bone_name)
for bone in bones_to_select:
pose_bone = pose.bones.get(bone)
@ -174,7 +173,7 @@ def flip_side_name(to_flip: str) -> str:
return prefix + replace + suffix + number
if __name__ == '__main__':
if __name__ == "__main__":
import doctest
print(f"Test result: {doctest.testmod()}")

View File

@ -1,20 +1,32 @@
import bpy
import os
from os.path import abspath, join
from bpy.types import (AddonPreferences, PointerProperty, PropertyGroup)
from bpy.props import (BoolProperty, StringProperty, CollectionProperty,
EnumProperty, IntProperty)
from asset_library.constants import (DATA_TYPES, DATA_TYPE_ITEMS,
ICONS, RESOURCES_DIR, LIBRARY_TYPE_DIR, LIBRARY_TYPES, ADAPTERS)
from bpy.types import AddonPreferences, PointerProperty, PropertyGroup
from bpy.props import (
BoolProperty,
StringProperty,
CollectionProperty,
EnumProperty,
IntProperty,
)
from asset_library.constants import (
DATA_TYPES,
DATA_TYPE_ITEMS,
ICONS,
RESOURCES_DIR,
LIBRARY_TYPE_DIR,
LIBRARY_TYPES,
ADAPTERS,
)
from asset_library.common.file_utils import import_module_from_path, norm_str
from asset_library.common.bl_utils import get_addon_prefs
from asset_library.common.library_cache import LibraryCache
from asset_library.common.catalog import Catalog
#from asset_library.common.functions import get_catalog_path
# from asset_library.common.functions import get_catalog_path
from pathlib import Path
import importlib
@ -22,120 +34,142 @@ import inspect
def update_library_config(self, context):
print('update_library_config not yet implemented')
print("update_library_config not yet implemented")
def update_library_path(self, context):
prefs = get_addon_prefs()
self['bundle_directory'] = str(self.library_path)
self["bundle_directory"] = str(self.library_path)
if not self.custom_bundle_name:
self['custom_bundle_name'] = self.name
self["custom_bundle_name"] = self.name
if not self.custom_bundle_directory:
custom_bundle_dir = Path(prefs.bundle_directory, self.library_name).resolve()
self['custom_bundle_directory'] = str(custom_bundle_dir)
self["custom_bundle_directory"] = str(custom_bundle_dir)
#if self.custom_bundle_directory:
# if self.custom_bundle_directory:
# self['custom_bundle_directory'] = abspath(bpy.path.abspath(self.custom_bundle_directory))
#else:
# else:
# bundle_directory = join(prefs.bundle_directory, norm_str(self.name))
# self['custom_bundle_directory'] = abspath(bundle_directory)
self.set_library_path()
def update_all_library_path(self, context):
#print('update_all_assetlib_paths')
# print('update_all_assetlib_paths')
prefs = get_addon_prefs()
#if self.custom_bundle_directory:
# if self.custom_bundle_directory:
# self['custom_bundle_directory'] = abspath(bpy.path.abspath(self.custom_bundle_directory))
for lib in prefs.libraries:
update_library_path(lib, context)
#lib.set_library_path()
# lib.set_library_path()
def get_library_type_items(self, context):
#prefs = get_addon_prefs()
# prefs = get_addon_prefs()
items = [('NONE', 'None', '', 0)]
items += [(norm_str(a.name, format=str.upper), a.name, "", i+1) for i, a in enumerate(LIBRARY_TYPES)]
items = [("NONE", "None", "", 0)]
items += [
(norm_str(a.name, format=str.upper), a.name, "", i + 1)
for i, a in enumerate(LIBRARY_TYPES)
]
return items
def get_adapters_items(self, context):
#prefs = get_addon_prefs()
items = [('NONE', 'None', '', 0)]
items += [(norm_str(a.name, format=str.upper), a.name, "", i+1) for i, a in enumerate(ADAPTERS)]
def get_adapters_items(self, context):
# prefs = get_addon_prefs()
items = [("NONE", "None", "", 0)]
items += [
(norm_str(a.name, format=str.upper), a.name, "", i + 1)
for i, a in enumerate(ADAPTERS)
]
return items
def get_library_items(self, context):
prefs = get_addon_prefs()
items = [('NONE', 'None', '', 0)]
items += [(l.name, l.name, "", i+1) for i, l in enumerate(prefs.libraries) if l != self]
items = [("NONE", "None", "", 0)]
items += [
(l.name, l.name, "", i + 1) for i, l in enumerate(prefs.libraries) if l != self
]
return items
def get_store_library_items(self, context):
#prefs = get_addon_prefs()
# prefs = get_addon_prefs()
#libraries = [l for l in prefs.libraries if l.merge_library == self.name]
# libraries = [l for l in prefs.libraries if l.merge_library == self.name]
return [(l.name, l.name, "", i) for i, l in enumerate([self] + self.merge_libraries)]
return [
(l.name, l.name, "", i) for i, l in enumerate([self] + self.merge_libraries)
]
class LibraryTypes(PropertyGroup):
def __iter__(self):
return (getattr(self, p) for p in self.bl_rna.properties.keys() if p not in ('rna_type', 'name'))
return (
getattr(self, p)
for p in self.bl_rna.properties.keys()
if p not in ("rna_type", "name")
)
class Adapters(PropertyGroup):
def __iter__(self):
return (getattr(self, p) for p in self.bl_rna.properties.keys() if p not in ('rna_type', 'name'))
return (
getattr(self, p)
for p in self.bl_rna.properties.keys()
if p not in ("rna_type", "name")
)
class AssetLibrary(PropertyGroup):
name : StringProperty(name='Name', default='Action Library', update=update_library_path)
id : StringProperty()
auto_bundle : BoolProperty(name='Auto Bundle', default=False)
expand : BoolProperty(name='Expand', default=False)
use : BoolProperty(name='Use', default=True, update=update_library_path)
data_type : EnumProperty(name='Type', items=DATA_TYPE_ITEMS, default='COLLECTION')
#template_image : StringProperty(default='', description='../{name}_image.png')
#template_video : StringProperty(default='', description='../{name}_video.mov')
#template_info : StringProperty(default='', description='../{name}_asset_info.json')
name: StringProperty(
name="Name", default="Action Library", update=update_library_path
)
id: StringProperty()
auto_bundle: BoolProperty(name="Auto Bundle", default=False)
expand: BoolProperty(name="Expand", default=False)
use: BoolProperty(name="Use", default=True, update=update_library_path)
data_type: EnumProperty(name="Type", items=DATA_TYPE_ITEMS, default="COLLECTION")
bundle_directory : StringProperty(
name="Bundle Directory",
subtype='DIR_PATH',
default=''
# template_image : StringProperty(default='', description='../{name}_image.png')
# template_video : StringProperty(default='', description='../{name}_video.mov')
# template_info : StringProperty(default='', description='../{name}_asset_info.json')
bundle_directory: StringProperty(
name="Bundle Directory", subtype="DIR_PATH", default=""
)
use_custom_bundle_directory : BoolProperty(default=False, update=update_library_path)
custom_bundle_directory : StringProperty(
use_custom_bundle_directory: BoolProperty(default=False, update=update_library_path)
custom_bundle_directory: StringProperty(
name="Bundle Directory",
subtype='DIR_PATH',
default='',
update=update_library_path
subtype="DIR_PATH",
default="",
update=update_library_path,
)
#use_merge : BoolProperty(default=False, update=update_library_path)
# use_merge : BoolProperty(default=False, update=update_library_path)
use_custom_bundle_name : BoolProperty(default=False, update=update_library_path)
custom_bundle_name : StringProperty(name='Merge Name', update=update_library_path)
#merge_library : EnumProperty(name='Merge Library', items=get_library_items, update=update_library_path)
#merge_name : StringProperty(name='Merge Name', update=update_library_path)
use_custom_bundle_name: BoolProperty(default=False, update=update_library_path)
custom_bundle_name: StringProperty(name="Merge Name", update=update_library_path)
# merge_library : EnumProperty(name='Merge Library', items=get_library_items, update=update_library_path)
# merge_name : StringProperty(name='Merge Name', update=update_library_path)
#Library when adding an asset to the library if merge with another
# Library when adding an asset to the library if merge with another
store_library: EnumProperty(items=get_store_library_items, name="Library")
template: StringProperty()
expand_extra : BoolProperty(name='Expand', default=False)
blend_depth : IntProperty(name='Blend Depth', default=1)
expand_extra: BoolProperty(name="Expand", default=False)
blend_depth: IntProperty(name="Blend Depth", default=1)
# source_directory : StringProperty(
# name="Path",
@ -144,15 +178,14 @@ class AssetLibrary(PropertyGroup):
# update=update_library_path
# )
# library_type : EnumProperty(items=library_type_ITEMS)
library_types: bpy.props.PointerProperty(type=LibraryTypes)
library_type_name: EnumProperty(items=get_library_type_items)
#library_type : EnumProperty(items=library_type_ITEMS)
library_types : bpy.props.PointerProperty(type=LibraryTypes)
library_type_name : EnumProperty(items=get_library_type_items)
adapters: bpy.props.PointerProperty(type=Adapters)
adapter_name: EnumProperty(items=get_adapters_items)
adapters : bpy.props.PointerProperty(type=Adapters)
adapter_name : EnumProperty(items=get_adapters_items)
parent_name : StringProperty()
parent_name: StringProperty()
# data_file_path : StringProperty(
# name="Path",
@ -160,7 +193,7 @@ class AssetLibrary(PropertyGroup):
# default='',
# )
#def __init__(self):
# def __init__(self):
# self.library_types.parent = self
@property
@ -171,20 +204,24 @@ class AssetLibrary(PropertyGroup):
@property
def merge_libraries(self):
prefs = get_addon_prefs()
return [l for l in prefs.libraries if l != self and (l.library_path == self.library_path)]
prefs = get_addon_prefs()
return [
l
for l in prefs.libraries
if l != self and (l.library_path == self.library_path)
]
@property
def child_libraries(self):
prefs = get_addon_prefs()
return [l for l in prefs.libraries if l != self and (l.parent == self)]
prefs = get_addon_prefs()
return [l for l in prefs.libraries if l != self and (l.parent == self)]
@property
def data_types(self):
data_type = self.data_type
if data_type == 'FILE':
data_type = 'COLLECTION'
return f'{data_type.lower()}s'
if data_type == "FILE":
data_type = "COLLECTION"
return f"{data_type.lower()}s"
@property
def library_type(self):
@ -207,10 +244,10 @@ class AssetLibrary(PropertyGroup):
prefs = get_addon_prefs()
asset_lib_ref = bpy.context.space_data.params.asset_library_ref
#TODO work also outside asset_library_area
# TODO work also outside asset_library_area
if asset_lib_ref not in prefs.libraries:
return None
return prefs.libraries[asset_lib_ref]
@property
@ -218,7 +255,7 @@ class AssetLibrary(PropertyGroup):
prefs = get_addon_prefs()
library_name = self.library_name
#if not self.use_custom_bundle_name:
# if not self.use_custom_bundle_name:
# library_name = norm_str(library_name)
if self.use_custom_bundle_directory:
@ -235,9 +272,9 @@ class AssetLibrary(PropertyGroup):
def library_name(self):
if self.use_custom_bundle_name:
return self.custom_bundle_name
return self.name
def read_catalog(self):
return Catalog(self.library_path).read()
@ -248,32 +285,32 @@ class AssetLibrary(PropertyGroup):
return LibraryCache.from_library(self).read()
def clear_library_path(self):
#print('Clear Library Path', self.name)
# print('Clear Library Path', self.name)
prefs = bpy.context.preferences
libs = prefs.filepaths.asset_libraries
#path = self.library_path.as_posix()
# path = self.library_path.as_posix()
for l in reversed(libs):
#lib_path = Path(l.path).resolve().as_posix()
# lib_path = Path(l.path).resolve().as_posix()
prev_name = self.get('asset_library') or self.library_name
prev_name = self.get("asset_library") or self.library_name
#print(l.name, prev_name)
# print(l.name, prev_name)
if (l.name == prev_name):
if l.name == prev_name:
index = list(libs).index(l)
try:
bpy.ops.preferences.asset_library_remove(index=index)
return
except AttributeError:
pass
#print('No library removed')
# print('No library removed')
def set_dict(self, data, obj=None):
""""Recursive method to set all attribute from a dict to this instance"""
""" "Recursive method to set all attribute from a dict to this instance"""
if obj is None:
obj = self
@ -281,46 +318,45 @@ class AssetLibrary(PropertyGroup):
# Make shure the input dict is not modidied
data = data.copy()
#print(obj)
# print(obj)
for key, value in data.items():
if isinstance(value, dict):
if 'name' in value:
setattr(obj, f'{key}_name', value.pop('name'))
if "name" in value:
setattr(obj, f"{key}_name", value.pop("name"))
#print('Nested value', getattr(obj, key))
# print('Nested value', getattr(obj, key))
self.set_dict(value, obj=getattr(obj, key))
elif key in obj.bl_rna.properties.keys():
if key == 'id':
if key == "id":
value = str(value)
elif key == 'custom_bundle_name':
if not 'use_custom_bundle_name' in data.values():
elif key == "custom_bundle_name":
if not "use_custom_bundle_name" in data.values():
obj["use_custom_bundle_name"] = True
elif isinstance(value, str):
value = os.path.expandvars(value)
value = os.path.expanduser(value)
#print('set attr', key, value)
# print('set attr', key, value)
setattr(obj, key, value)
#obj[key] = value
else:
print(f'Prop {key} of {obj} not exist')
# obj[key] = value
self['bundle_directory'] = str(self.library_path)
else:
print(f"Prop {key} of {obj} not exist")
self["bundle_directory"] = str(self.library_path)
if not self.custom_bundle_name:
self['custom_bundle_name'] = self.name
self["custom_bundle_name"] = self.name
# self.library_type_name = data['library_type']
# if not self.library_type:
# print(f"No library_type named {data['library_type']}")
# return
# for key, value in data.items():
# if key == 'options':
@ -329,7 +365,7 @@ class AssetLibrary(PropertyGroup):
# elif key in self.bl_rna.properties.keys():
# if key == 'id':
# value = str(value)
# if key == 'custom_bundle_name':
# if not 'use_custom_bundle_name' in data.values():
# self["use_custom_bundle_name"] = True
@ -337,22 +373,26 @@ class AssetLibrary(PropertyGroup):
# self[key] = value
def to_dict(self):
data = {p: getattr(self, p) for p in self.bl_rna.properties.keys() if p !='rna_type'}
data = {
p: getattr(self, p)
for p in self.bl_rna.properties.keys()
if p != "rna_type"
}
if self.library_type:
data['library_type'] = self.library_type.to_dict()
data['library_type']['name'] = data.pop('library_type_name')
del data['library_types']
data["library_type"] = self.library_type.to_dict()
data["library_type"]["name"] = data.pop("library_type_name")
del data["library_types"]
if self.adapter:
data['adapter'] = self.adapter.to_dict()
data['adapter']['name'] = data.pop('adapter_name')
del data['adapters']
data["adapter"] = self.adapter.to_dict()
data["adapter"]["name"] = data.pop("adapter_name")
del data["adapters"]
return data
def set_library_path(self):
'''Update the Blender Preference Filepaths tab with the addon libraries'''
"""Update the Blender Preference Filepaths tab with the addon libraries"""
prefs = bpy.context.preferences
name = self.library_name
@ -360,7 +400,6 @@ class AssetLibrary(PropertyGroup):
self.clear_library_path()
if not self.use or not lib_path:
# if all(not l.use for l in self.merge_libraries):
# self.clear_library_path()
@ -370,7 +409,7 @@ class AssetLibrary(PropertyGroup):
# if self.get('asset_library'):
# #print('old_name', self['asset_library'])
# lib = prefs.filepaths.asset_libraries.get(self['asset_library'])
# if not lib:
# #print('keys', prefs.filepaths.asset_libraries.keys())
# #print('name', name)
@ -380,19 +419,19 @@ class AssetLibrary(PropertyGroup):
# Create the Asset Library Path
lib = prefs.filepaths.asset_libraries.get(name)
if not lib:
#print(f'Creating the lib {name}')
# print(f'Creating the lib {name}')
try:
bpy.ops.preferences.asset_library_add(directory=str(lib_path))
except AttributeError:
return
lib = prefs.filepaths.asset_libraries[-1]
lib.name = name
self['asset_library'] = name
self["asset_library"] = name
lib.path = str(lib_path)
@property
def is_user(self):
prefs = get_addon_prefs()
@ -403,24 +442,24 @@ class AssetLibrary(PropertyGroup):
prefs = get_addon_prefs()
return self in prefs.env_libraries.values()
def add_row(self, layout, data=None, prop=None, label='',
boolean=None, factor=0.39):
'''Act like the use_property_split but with more control'''
def add_row(
self, layout, data=None, prop=None, label="", boolean=None, factor=0.39
):
"""Act like the use_property_split but with more control"""
enabled = True
split = layout.split(factor=factor, align=True)
row = split.row(align=False)
row.use_property_split = False
row.alignment= 'RIGHT'
row.alignment = "RIGHT"
row.label(text=str(label))
if boolean:
boolean_data = self
if isinstance(boolean, (list, tuple)):
boolean_data, boolean = boolean
row.prop(boolean_data, boolean, text='')
row.prop(boolean_data, boolean, text="")
enabled = getattr(boolean_data, boolean)
row = split.row(align=True)
@ -429,69 +468,72 @@ class AssetLibrary(PropertyGroup):
if isinstance(data, str):
row.label(text=data)
else:
row.prop(data or self, prop, text='')
row.prop(data or self, prop, text="")
return split
def draw_operators(self, layout):
row = layout.row(align=True)
row.alignment = 'RIGHT'
row.prop(self, 'library_type_name', text='')
row.prop(self, 'auto_bundle', text='', icon='UV_SYNC_SELECT')
row.alignment = "RIGHT"
row.prop(self, "library_type_name", text="")
row.prop(self, "auto_bundle", text="", icon="UV_SYNC_SELECT")
row.operator("assetlib.diff", text='', icon='FILE_REFRESH').name = self.name
row.operator("assetlib.diff", text="", icon="FILE_REFRESH").name = self.name
op = row.operator("assetlib.bundle", icon='MOD_BUILD', text='')
op = row.operator("assetlib.bundle", icon="MOD_BUILD", text="")
op.name = self.name
layout.separator(factor=3)
def draw(self, layout):
prefs = get_addon_prefs()
#box = layout.box()
# box = layout.box()
row = layout.row(align=True)
#row.use_property_split = False
# row.use_property_split = False
#row.alignment = 'LEFT'
# row.alignment = 'LEFT'
icon = "DISCLOSURE_TRI_DOWN" if self.expand else "DISCLOSURE_TRI_RIGHT"
row.prop(self, 'expand', icon=icon, emboss=False, text='')
row.prop(self, "expand", icon=icon, emboss=False, text="")
if self.is_user:
row.prop(self, 'use', text='')
row.prop(self, 'data_type', icon_only=True, emboss=False)
row.prop(self, 'name', text='')
row.prop(self, "use", text="")
row.prop(self, "data_type", icon_only=True, emboss=False)
row.prop(self, "name", text="")
self.draw_operators(row)
index = list(prefs.user_libraries).index(self)
row.operator("assetlib.remove_user_library", icon="X", text='', emboss=False).index = index
row.operator(
"assetlib.remove_user_library", icon="X", text="", emboss=False
).index = index
else:
row.prop(self, 'use', text='')
row.prop(self, "use", text="")
row.label(icon=ICONS[self.data_type])
#row.label(text=self.name)
# row.label(text=self.name)
subrow = row.row(align=True)
subrow.alignment = 'LEFT'
subrow.prop(self, 'expand', emboss=False, text=self.name)
#row.separator_spacer()
subrow.alignment = "LEFT"
subrow.prop(self, "expand", emboss=False, text=self.name)
# row.separator_spacer()
self.draw_operators(row)
sub_row = row.row()
sub_row.enabled = False
sub_row.label(icon='FAKE_USER_ON')
sub_row.label(icon="FAKE_USER_ON")
if self.expand:
col = layout.column(align=False)
col.use_property_split = True
#row = col.row(align=True)
# row = col.row(align=True)
row = self.add_row(col,
prop="custom_bundle_name",
boolean="use_custom_bundle_name",
label='Custom Bundle Name')
row = self.add_row(
col,
prop="custom_bundle_name",
boolean="use_custom_bundle_name",
label="Custom Bundle Name",
)
row.enabled = not self.use_custom_bundle_directory
@ -499,31 +541,32 @@ class AssetLibrary(PropertyGroup):
if self.use_custom_bundle_directory:
prop = "custom_bundle_directory"
self.add_row(col, prop=prop,
boolean="use_custom_bundle_directory",
label='Custom Bundle Directory',
self.add_row(
col,
prop=prop,
boolean="use_custom_bundle_directory",
label="Custom Bundle Directory",
)
col.prop(self, "blend_depth")
#subcol = col.column(align=True)
#subcol.prop(self, "template_info", text='Template Info', icon='COPY_ID')
#subcol.prop(self, "template_image", text='Template Image', icon='COPY_ID')
#subcol.prop(self, "template_video", text='Template Video', icon='COPY_ID')
# subcol = col.column(align=True)
# subcol.prop(self, "template_info", text='Template Info', icon='COPY_ID')
# subcol.prop(self, "template_image", text='Template Image', icon='COPY_ID')
# subcol.prop(self, "template_video", text='Template Video', icon='COPY_ID')
if self.library_type:
col.separator()
self.library_type.draw_prefs(col)
for lib in self.child_libraries:
lib.draw(layout)
col.separator()
class Collections:
'''Util Class to merge multiple collections'''
"""Util Class to merge multiple collections"""
collections = []
@ -531,17 +574,17 @@ class Collections:
self.collections = collection
for col in collection:
#print('Merge methods')
# print('Merge methods')
for attr in dir(col):
if attr.startswith('_'):
if attr.startswith("_"):
continue
value = getattr(col, attr)
#if not callable(value):
# if not callable(value):
# continue
setattr(self, attr, value)
def __contains__(self, item):
if isinstance(item, str):
return item in self.to_dict()
@ -556,12 +599,12 @@ class Collections:
return self.to_list()[item]
else:
return self.to_dict()[item]
def get(self, item, fallback=None):
return self.to_dict().get(item) or fallback
def to_dict(self):
return {k:v for c in self.collections for k, v in c.items()}
return {k: v for c in self.collections for k, v in c.items()}
def to_list(self):
return [v for c in self.collections for v in c.values()]
@ -576,11 +619,11 @@ class Collections:
if not c:
return item in self
return list(c.values()).index(item)
#class AssetLibraryOptions(PropertyGroup):
# class AssetLibraryOptions(PropertyGroup):
# pass
@ -593,78 +636,80 @@ class AssetLibraryPrefs(AddonPreferences):
preview_modal = False
add_asset_dict = {}
#action : bpy.props.PointerProperty(type=AssetLibraryPath)
#asset : bpy.props.PointerProperty(type=AssetLibraryPath)
#library_types = {}
# action : bpy.props.PointerProperty(type=AssetLibraryPath)
# asset : bpy.props.PointerProperty(type=AssetLibraryPath)
# library_types = {}
author: StringProperty(default=os.getlogin())
image_player: StringProperty(default='')
video_player: StringProperty(default='')
image_player: StringProperty(default="")
video_player: StringProperty(default="")
library_type_directory : StringProperty(name="Library Type Directory", subtype='DIR_PATH')
adapter_directory : StringProperty(name="Adapter Directory", subtype='DIR_PATH')
library_type_directory: StringProperty(
name="Library Type Directory", subtype="DIR_PATH"
)
adapter_directory: StringProperty(name="Adapter Directory", subtype="DIR_PATH")
env_libraries : CollectionProperty(type=AssetLibrary)
user_libraries : CollectionProperty(type=AssetLibrary)
env_libraries: CollectionProperty(type=AssetLibrary)
user_libraries: CollectionProperty(type=AssetLibrary)
expand_settings: BoolProperty(default=False)
bundle_directory : StringProperty(
name="Path",
subtype='DIR_PATH',
default='',
update=update_all_library_path
bundle_directory: StringProperty(
name="Path", subtype="DIR_PATH", default="", update=update_all_library_path
)
config_directory : StringProperty(
config_directory: StringProperty(
name="Config Path",
subtype='FILE_PATH',
default=str(RESOURCES_DIR/"asset_library_config.json"),
update=update_library_config
subtype="FILE_PATH",
default=str(RESOURCES_DIR / "asset_library_config.json"),
update=update_library_config,
)
def load_library_types(self):
from asset_library.library_types.library_type import LibraryType
print('Asset Library: Load Library Types')
print("Asset Library: Load Library Types")
LIBRARY_TYPES.clear()
library_type_files = list(LIBRARY_TYPE_DIR.glob('*.py'))
library_type_files = list(LIBRARY_TYPE_DIR.glob("*.py"))
if self.library_type_directory:
user_LIBRARY_TYPE_DIR = Path(self.library_type_directory)
if user_LIBRARY_TYPE_DIR.exists():
library_type_files += list(user_LIBRARY_TYPE_DIR.glob('*.py'))
library_type_files += list(user_LIBRARY_TYPE_DIR.glob("*.py"))
for library_type_file in library_type_files:
if library_type_file.stem.startswith('_'):
if library_type_file.stem.startswith("_"):
continue
mod = import_module_from_path(library_type_file)
#print(library_type_file)
# print(library_type_file)
for name, obj in inspect.getmembers(mod):
if not inspect.isclass(obj):
if not inspect.isclass(obj):
continue
#print(obj.__bases__)
# print(obj.__bases__)
if not LibraryType in obj.__mro__:
continue
# Non registering base library_type
if obj is LibraryType or obj.name in (a.name for a in LIBRARY_TYPES):
if obj is LibraryType or obj.name in (a.name for a in LIBRARY_TYPES):
continue
try:
print(f'Register Plugin {name}')
print(f"Register Plugin {name}")
bpy.utils.register_class(obj)
setattr(LibraryTypes, norm_str(obj.name), bpy.props.PointerProperty(type=obj))
setattr(
LibraryTypes,
norm_str(obj.name),
bpy.props.PointerProperty(type=obj),
)
LIBRARY_TYPES.append(obj)
except Exception as e:
print(f'Could not register library_type {name}')
print(f"Could not register library_type {name}")
print(e)
def load_adapters(self):
return
@ -676,52 +721,60 @@ class AssetLibraryPrefs(AddonPreferences):
prefs = get_addon_prefs()
layout = self.layout
#layout.use_property_split = True
# layout.use_property_split = True
main_col = layout.column(align=False)
box = main_col.box()
row = box.row(align=True)
icon = "DISCLOSURE_TRI_DOWN" if self.expand_settings else "DISCLOSURE_TRI_RIGHT"
row.prop(self, 'expand_settings', icon=icon, emboss=False, text='')
row.label(icon='PREFERENCES')
row.label(text='Settings')
#row.separator_spacer()
row.prop(self, "expand_settings", icon=icon, emboss=False, text="")
row.label(icon="PREFERENCES")
row.label(text="Settings")
# row.separator_spacer()
subrow = row.row()
subrow.alignment = 'RIGHT'
subrow.operator("assetlib.reload_addon", text='Reload Addon')
subrow.alignment = "RIGHT"
subrow.operator("assetlib.reload_addon", text="Reload Addon")
if prefs.expand_settings:
col = box.column(align=True)
col.use_property_split = True
#col.prop(self, 'use_single_path', text='Single Path')
col.prop(self, 'bundle_directory', text='Bundle Directory')
# col.prop(self, 'use_single_path', text='Single Path')
col.prop(self, "bundle_directory", text="Bundle Directory")
col.separator()
col.prop(self, 'library_type_directory')
col.prop(self, 'config_directory')
col.prop(self, "library_type_directory")
col.prop(self, "config_directory")
col.separator()
#col.prop(self, 'template_info', text='Asset Description Template', icon='COPY_ID')
# col.prop(self, 'template_info', text='Asset Description Template', icon='COPY_ID')
#col.separator()
# col.separator()
#col.prop(self, 'template_image', text='Template Image', icon='COPY_ID')
col.prop(self, 'image_player', text='Image Player') #icon='OUTLINER_OB_IMAGE'
# col.prop(self, 'template_image', text='Template Image', icon='COPY_ID')
col.prop(
self, "image_player", text="Image Player"
) # icon='OUTLINER_OB_IMAGE'
#col.separator()
# col.separator()
#col.prop(self, 'template_video', text='Template Video', icon='COPY_ID')
col.prop(self, 'video_player', text='Video Player') #icon='FILE_MOVIE'
# col.prop(self, 'template_video', text='Template Video', icon='COPY_ID')
col.prop(self, "video_player", text="Video Player") # icon='FILE_MOVIE'
col.separator()
col.operator("assetlib.add_user_library", text='Bundle All Libraries', icon='MOD_BUILD')
col.operator(
"assetlib.add_user_library",
text="Bundle All Libraries",
icon="MOD_BUILD",
)
for lib in self.libraries:# list(self.env_libraries) + list(self.user_libraries):
for (
lib
) in self.libraries: # list(self.env_libraries) + list(self.user_libraries):
if lib.parent:
continue
@ -729,46 +782,48 @@ class AssetLibraryPrefs(AddonPreferences):
lib.draw(box)
row = main_col.row()
row.alignment = 'RIGHT'
row.operator("assetlib.add_user_library", icon="ADD", text='', emboss=False)
row.alignment = "RIGHT"
row.operator("assetlib.add_user_library", icon="ADD", text="", emboss=False)
classes = [
LibraryTypes,
Adapters,
#ConformAssetLibrary,
# ConformAssetLibrary,
AssetLibrary,
AssetLibraryPrefs,
]
def register():
for cls in classes:
bpy.utils.register_class(cls)
prefs = get_addon_prefs()
# Read Env and override preferences
bundle_dir = os.getenv('ASSETLIB_BUNDLE_DIR')
bundle_dir = os.getenv("ASSETLIB_BUNDLE_DIR")
if bundle_dir:
prefs['bundle_directory'] = os.path.expandvars(bundle_dir)
prefs["bundle_directory"] = os.path.expandvars(bundle_dir)
config_dir = os.getenv('ASSETLIB_CONFIG_DIR')
config_dir = os.getenv("ASSETLIB_CONFIG_DIR")
if config_dir:
prefs['config_directory'] = os.path.expandvars(config_dir)
prefs["config_directory"] = os.path.expandvars(config_dir)
LIBRARY_TYPE_DIR = os.getenv('ASSETLIB_LIBRARY_TYPE_DIR')
LIBRARY_TYPE_DIR = os.getenv("ASSETLIB_LIBRARY_TYPE_DIR")
if LIBRARY_TYPE_DIR:
prefs['library_type_directory'] = os.path.expandvars(LIBRARY_TYPE_DIR)
ADAPTER_DIR = os.getenv('ASSETLIB_ADAPTER_DIR')
prefs["library_type_directory"] = os.path.expandvars(LIBRARY_TYPE_DIR)
ADAPTER_DIR = os.getenv("ASSETLIB_ADAPTER_DIR")
if ADAPTER_DIR:
prefs['adapter_directory'] = os.path.expandvars(ADAPTER_DIR)
prefs["adapter_directory"] = os.path.expandvars(ADAPTER_DIR)
prefs.load_library_types()
prefs.load_adapters()
def unregister():
for cls in reversed(classes + LIBRARY_TYPES):
bpy.utils.unregister_class(cls)
LIBRARY_TYPES.clear()
LIBRARY_TYPES.clear()

View File

@ -1,4 +1,3 @@
import bpy
from pathlib import Path
from asset_library.common.file_utils import read_file, write_file
@ -11,27 +10,24 @@ class AssetCache:
def __init__(self, file_cache, data):
self.file_cache = file_cache
self._data = data
self.catalog = data['catalog']
self.author = data.get('author', '')
self.description = data.get('description', '')
self.tags = data.get('tags', [])
self.type = data.get('type')
self.name = data['name']
self._metadata = data.get('metadata', {})
self.catalog = data["catalog"]
self.author = data.get("author", "")
self.description = data.get("description", "")
self.tags = data.get("tags", [])
self.type = data.get("type")
self.name = data["name"]
self._metadata = data.get("metadata", {})
@property
def filepath(self):
return self.file_cache.filepath
@property
def metadata(self):
metadata = {
'.library_id': self.library.id,
'.filepath': self.filepath
}
metadata = {".library_id": self.library.id, ".filepath": self.filepath}
metadata.update(self.metadata)
@ -39,7 +35,7 @@ class AssetCache:
@property
def norm_name(self):
return self.name.replace(' ', '_').lower()
return self.name.replace(" ", "_").lower()
def to_dict(self):
return dict(
@ -49,23 +45,23 @@ class AssetCache:
description=self.description,
tags=self.tags,
type=self.type,
name=self.name
name=self.name,
)
def __str__(self):
return f'AssetCache(name={self.name}, type={self.type}, catalog={self.catalog})'
return f"AssetCache(name={self.name}, type={self.type}, catalog={self.catalog})"
class FileCache:
def __init__(self, library_cache, data):
self.library_cache = library_cache
self.filepath = data['filepath']
self.modified = data.get('modified', time.time_ns())
self.filepath = data["filepath"]
self.modified = data.get("modified", time.time_ns())
self._data = []
for asset_cache_data in data.get('assets', []):
for asset_cache_data in data.get("assets", []):
self.add(asset_cache_data)
def add(self, asset_cache_data):
@ -77,7 +73,7 @@ class FileCache:
filepath=self.filepath.as_posix(),
modified=self.modified,
library_id=self.library_cache.library.id,
assets=[asset_cache.to_dict() for asset_cache in self]
assets=[asset_cache.to_dict() for asset_cache in self],
)
def __iter__(self):
@ -87,14 +83,14 @@ class FileCache:
return self._data[key]
def __str__(self):
return f'FileCache(filepath={self.filepath})'
return f"FileCache(filepath={self.filepath})"
class AssetCacheDiff:
def __init__(self, library_diff, asset_cache, operation):
self.library_cache = library_cache
self.filepath = data['filepath']
self.filepath = data["filepath"]
self.operation = operation
@ -102,7 +98,7 @@ class LibraryCacheDiff:
def __init__(self, filepath=None):
self.filepath = filepath
self._data = []
self._data = []
def add(self, asset_diff):
asset_diff = AssetCacheDiff(self, asset_diff)
@ -111,23 +107,23 @@ class LibraryCacheDiff:
def set(self, asset_diffs):
for asset_diff in asset_diffs:
self.add(asset_diff)
def read(self):
print(f'Read cache from {self.filepath}')
print(f"Read cache from {self.filepath}")
for asset_diff_data in read_file(self.filepath):
self.add(asset_diff_data)
return self
def group_by(self, key):
'''Return groups of file cache diff using the key provided'''
"""Return groups of file cache diff using the key provided"""
data = list(self).sort(key=key)
return groupby(data, key=key)
def __iter__(self):
return iter(self._data)
def __len__(self):
return len(self._data)
@ -138,13 +134,13 @@ class LibraryCacheDiff:
class LibraryCache:
def __init__(self, directory, id):
self.directory = directory
self.id = id
self._data = []
@classmethod
def from_library(cls, library):
def from_library(cls, library):
return cls(library.library_path, library.id)
@property
@ -155,19 +151,19 @@ class LibraryCache:
def filepath(self):
"""Get the filepath of the library json file relative to the library"""
return self.directory / self.filename
@property
def asset_caches(self):
'''Return an iterator to get all asset caches'''
"""Return an iterator to get all asset caches"""
return (asset_cache for file_cache in self for asset_cache in file_cache)
@property
def tmp_filepath(self):
return Path(bpy.app.tempdir) / self.filename
def read(self):
print(f'Read cache from {self.filepath}')
print(f"Read cache from {self.filepath}")
for file_cache_data in read_file(self.filepath):
self.add(file_cache_data)
@ -178,7 +174,7 @@ class LibraryCache:
if temp:
filepath = self.tmp_filepath
print(f'Write cache file to {filepath}')
print(f"Write cache file to {filepath}")
write_file(filepath, self._data)
return filepath
@ -188,25 +184,28 @@ class LibraryCache:
self._data.append(file_cache)
def unflatten_cache(self, cache):
""" Return a new unflattten list of asset data
"""Return a new unflattten list of asset data
grouped by filepath"""
new_cache = []
cache = deepcopy(cache)
cache.sort(key=lambda x : x['filepath'])
groups = groupby(cache, key=lambda x : x['filepath'])
cache.sort(key=lambda x: x["filepath"])
groups = groupby(cache, key=lambda x: x["filepath"])
keys = ['filepath', 'modified', 'library_id']
keys = ["filepath", "modified", "library_id"]
for _, asset_datas in groups:
asset_datas = list(asset_datas)
#print(asset_datas[0])
# print(asset_datas[0])
asset_info = {k:asset_datas[0][k] for k in keys}
asset_info['assets'] = [{k:v for k, v in a.items() if k not in keys+['operation']} for a in asset_datas]
asset_info = {k: asset_datas[0][k] for k in keys}
asset_info["assets"] = [
{k: v for k, v in a.items() if k not in keys + ["operation"]}
for a in asset_datas
]
new_cache.append(asset_info)
@ -218,42 +217,59 @@ class LibraryCache:
cache = self.read()
cache_dict = {f"{a['filepath']}/{a['name']}": a for a in cache.asset_caches}
new_cache_dict = {f"{a['filepath']}/{a['name']}" : a for a in new_cache.asset_caches}
new_cache_dict = {
f"{a['filepath']}/{a['name']}": a for a in new_cache.asset_caches
}
assets_added = [AssetCacheDiff(v, 'ADD') for k, v in new_cache.items() if k not in cache]
assets_removed = [AssetCacheDiff(v, 'REMOVED') for k, v in cache.items() if k not in new_cache]
assets_modified = [AssetCacheDiff(v, 'MODIFIED') for k, v in cache.items() if v not in assets_removed and v!= new_cache[k]]
assets_added = [
AssetCacheDiff(v, "ADD") for k, v in new_cache.items() if k not in cache
]
assets_removed = [
AssetCacheDiff(v, "REMOVED") for k, v in cache.items() if k not in new_cache
]
assets_modified = [
AssetCacheDiff(v, "MODIFIED")
for k, v in cache.items()
if v not in assets_removed and v != new_cache[k]
]
if assets_added:
print(f'{len(assets_added)} Assets Added \n{tuple(a.name for a in assets_added[:10])}...\n')
print(
f"{len(assets_added)} Assets Added \n{tuple(a.name for a in assets_added[:10])}...\n"
)
if assets_removed:
print(f'{len(assets_removed)} Assets Removed \n{tuple(a.name for a in assets_removed[:10])}...\n')
print(
f"{len(assets_removed)} Assets Removed \n{tuple(a.name for a in assets_removed[:10])}...\n"
)
if assets_modified:
print(f'{len(assets_modified)} Assets Modified \n{tuple(a.name for a in assets_modified[:10])}...\n')
print(
f"{len(assets_modified)} Assets Modified \n{tuple(a.name for a in assets_modified[:10])}...\n"
)
cache_diff = LibraryCacheDiff()
cache_diff.set(assets_added+assets_removed+assets_modified)
cache_diff.set(assets_added + assets_removed + assets_modified)
if not len(LibraryCacheDiff):
print('No change in the library')
print("No change in the library")
return cache_diff
def __len__(self):
return len(self._data)
def __iter__(self):
return iter(self._data)
def __getitem__(self, key):
return self._data[key]
def __str__(self):
return f'LibraryCache(library={self.library.name})'
return f"LibraryCache(library={self.library.name})"
print()
prefs = bpy.context.preferences.addons['asset_library'].preferences
prefs = bpy.context.preferences.addons["asset_library"].preferences
library = prefs.env_libraries[0]
@ -264,7 +280,7 @@ print(data)
print(library_cache[0][0])
#library_cache.diff(library.library_type.fetch())
# library_cache.diff(library.library_type.fetch())
#print(library_cache[0])
# print(library_cache[0])