Compare commits
2
Commits
DEV
..
black-format
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ab2d4e3d9 | ||
|
|
3d65bb6e4d |
@@ -0,0 +1,2 @@
|
|||||||
|
# Migrate code style to Black
|
||||||
|
3d65bb6e4db5d1a81e48a724c6059be317ac35a9
|
||||||
+53
-34
@@ -8,62 +8,81 @@ Extending features of the Asset Browser for a studio use.
|
|||||||
bl_info = {
|
bl_info = {
|
||||||
"name": "Asset Library",
|
"name": "Asset Library",
|
||||||
"description": "Asset Library based on the Asset Browser.",
|
"description": "Asset Library based on the Asset Browser.",
|
||||||
"author": "Christophe Seux",
|
"author": "Sybren A. Stüvel, Clement Ducarteron, Christophe Seux, Samuel Bernou",
|
||||||
"version": (2, 0),
|
"version": (2, 0),
|
||||||
"blender": (4, 0, 2),
|
"blender": (3, 3, 0),
|
||||||
"warning": "In development, things may change",
|
"warning": "In development, things may change",
|
||||||
"location": "Asset Browser",
|
"location": "Asset Browser -> Animations, and 3D Viewport -> Animation panel",
|
||||||
"category": "Import-Export",
|
"category": "Animation",
|
||||||
}
|
}
|
||||||
|
|
||||||
import sys
|
# from typing import List, Tuple
|
||||||
|
|
||||||
from . import operators, properties, ui, preferences, data_type
|
|
||||||
from .core.lib_utils import load_libraries, update_library_path
|
|
||||||
|
|
||||||
|
|
||||||
bl_modules = (
|
from asset_library import pose
|
||||||
operators,
|
from asset_library import action
|
||||||
properties,
|
from asset_library import collection
|
||||||
ui,
|
from asset_library import file
|
||||||
preferences,
|
from asset_library import gui, keymaps, preferences, operators
|
||||||
data_type
|
from asset_library import constants
|
||||||
)
|
|
||||||
|
# from asset_library.common.library_type import LibraryType
|
||||||
|
from asset_library.common.bl_utils import get_addon_prefs
|
||||||
|
from asset_library.common.functions import set_env_libraries
|
||||||
|
from asset_library.common.template import Template
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
# Reload Modules from inside Blender
|
|
||||||
if "bpy" in locals():
|
if "bpy" in locals():
|
||||||
|
print("Reload Addon Asset Library")
|
||||||
|
|
||||||
import importlib
|
import importlib
|
||||||
|
|
||||||
for mod in modules:
|
importlib.reload(constants)
|
||||||
importlib.reload(mod)
|
importlib.reload(gui)
|
||||||
|
importlib.reload(keymaps)
|
||||||
|
|
||||||
|
importlib.reload(preferences)
|
||||||
|
importlib.reload(operators)
|
||||||
|
importlib.reload(constants)
|
||||||
|
|
||||||
|
importlib.reload(action)
|
||||||
|
importlib.reload(file)
|
||||||
|
importlib.reload(collection)
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
# addon_keymaps: List[Tuple[bpy.types.KeyMap, bpy.types.KeyMapItem]] = []
|
||||||
|
|
||||||
|
bl_modules = (operators, pose, action, collection, file, keymaps, gui, preferences)
|
||||||
|
|
||||||
|
|
||||||
def load_handler():
|
def load_handler():
|
||||||
print('load_handler')
|
print("load_handler")
|
||||||
load_libraries()
|
|
||||||
update_library_path()
|
|
||||||
#set_env_libraries()
|
|
||||||
#bpy.ops.assetlib.set_paths(all=True)
|
|
||||||
|
|
||||||
#if not bpy.app.background:
|
set_env_libraries()
|
||||||
# bpy.ops.assetlib.bundle(blocking=False, mode='AUTO_BUNDLE')
|
bpy.ops.assetlib.set_paths(all=True)
|
||||||
|
|
||||||
|
if not bpy.app.background:
|
||||||
|
bpy.ops.assetlib.bundle(blocking=False, mode="AUTO_BUNDLE")
|
||||||
|
|
||||||
|
|
||||||
|
def register() -> None:
|
||||||
|
|
||||||
def register():
|
for m in bl_modules:
|
||||||
"""Register the addon Asset Library for Blender"""
|
m.register()
|
||||||
|
|
||||||
for mod in bl_modules:
|
|
||||||
mod.register()
|
|
||||||
|
|
||||||
|
# prefs = get_addon_prefs()
|
||||||
|
|
||||||
bpy.app.timers.register(load_handler, first_interval=1)
|
bpy.app.timers.register(load_handler, first_interval=1)
|
||||||
|
|
||||||
|
|
||||||
def unregister():
|
def unregister() -> None:
|
||||||
"""Unregister the addon Asset Library for Blender"""
|
prefs = get_addon_prefs()
|
||||||
|
bpy.utils.previews.remove(prefs.previews)
|
||||||
|
|
||||||
for mod in reversed(bl_modules):
|
for m in reversed(bl_modules):
|
||||||
mod.unregister()
|
m.unregister()
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
from asset_library.action import (
|
||||||
|
gui,
|
||||||
|
keymaps,
|
||||||
|
clear_asset,
|
||||||
|
concat_preview,
|
||||||
|
operators,
|
||||||
|
properties,
|
||||||
|
rename_pose,
|
||||||
|
# render_preview
|
||||||
|
)
|
||||||
|
|
||||||
|
if "bpy" in locals():
|
||||||
|
import importlib
|
||||||
|
|
||||||
|
importlib.reload(gui)
|
||||||
|
importlib.reload(keymaps)
|
||||||
|
importlib.reload(clear_asset)
|
||||||
|
importlib.reload(concat_preview)
|
||||||
|
importlib.reload(operators)
|
||||||
|
importlib.reload(properties)
|
||||||
|
importlib.reload(rename_pose)
|
||||||
|
# importlib.reload(render_preview)
|
||||||
|
|
||||||
|
import bpy
|
||||||
|
|
||||||
|
|
||||||
|
def register():
|
||||||
|
operators.register()
|
||||||
|
keymaps.register()
|
||||||
|
|
||||||
|
|
||||||
|
def unregister():
|
||||||
|
operators.unregister()
|
||||||
|
keymaps.unregister()
|
||||||
@@ -1,12 +1,11 @@
|
|||||||
|
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
#sys.path.append(str(Path(__file__).parents[3]))
|
|
||||||
|
|
||||||
from asset_library.data_type.action.concat_preview import mosaic_export
|
# 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
|
from asset_library.common.file_utils import open_file
|
||||||
from asset_library.data_type.action.functions import reset_bone, get_keyframes
|
from asset_library.action.functions import reset_bone, get_keyframes
|
||||||
from asset_library.common.functions import read_catalog
|
from asset_library.common.functions import read_catalog
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
@@ -19,17 +18,19 @@ import subprocess
|
|||||||
from tempfile import gettempdir
|
from tempfile import gettempdir
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def rm_tree(pth):
|
def rm_tree(pth):
|
||||||
pth = Path(pth)
|
pth = Path(pth)
|
||||||
for child in pth.glob('*'):
|
for child in pth.glob("*"):
|
||||||
if child.is_file():
|
if child.is_file():
|
||||||
child.unlink()
|
child.unlink()
|
||||||
else:
|
else:
|
||||||
rm_tree(child)
|
rm_tree(child)
|
||||||
pth.rmdir()
|
pth.rmdir()
|
||||||
|
|
||||||
def render_preview(directory, asset_catalog, render_actions, publish_actions, remove_folder):
|
|
||||||
|
def render_preview(
|
||||||
|
directory, asset_catalog, render_actions, publish_actions, remove_folder
|
||||||
|
):
|
||||||
|
|
||||||
scn = bpy.context.scene
|
scn = bpy.context.scene
|
||||||
rnd = bpy.context.scene.render
|
rnd = bpy.context.scene.render
|
||||||
@@ -39,27 +40,32 @@ def render_preview(directory, asset_catalog, render_actions, publish_actions, re
|
|||||||
blendfile = Path(bpy.data.filepath)
|
blendfile = Path(bpy.data.filepath)
|
||||||
asset_catalog_data = read_catalog(asset_catalog)
|
asset_catalog_data = read_catalog(asset_catalog)
|
||||||
|
|
||||||
anim_render_dir = 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)
|
anim_render_dir.mkdir(exist_ok=True, parents=True)
|
||||||
|
|
||||||
preview_render_dir = Path(directory) / 'preview'
|
preview_render_dir = Path(directory) / "preview"
|
||||||
|
|
||||||
if preview_render_dir.exists() and remove_folder:
|
if preview_render_dir.exists() and remove_folder:
|
||||||
rm_tree(preview_render_dir)
|
rm_tree(preview_render_dir)
|
||||||
|
|
||||||
preview_render_dir.mkdir(exist_ok=True, parents=True)
|
preview_render_dir.mkdir(exist_ok=True, parents=True)
|
||||||
for i in ('anim', 'pose'):
|
for i in ("anim", "pose"):
|
||||||
Path(preview_render_dir / i).mkdir(exist_ok=True, parents=True)
|
Path(preview_render_dir / i).mkdir(exist_ok=True, parents=True)
|
||||||
|
|
||||||
for f in preview_render_dir.rglob('*'):
|
for f in preview_render_dir.rglob("*"):
|
||||||
if f.is_dir():
|
if f.is_dir():
|
||||||
print(f'{f} is dir. Skipped.')
|
print(f"{f} is dir. Skipped.")
|
||||||
continue
|
continue
|
||||||
if all(i not in f.parts for i in ('anim', 'pose')) and f.parent.parts[-1] != 'preview':
|
if (
|
||||||
print(f'{f} is out of pipe. Approved or Rtk pictures. Skipped.')
|
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
|
continue
|
||||||
if not any(f.stem.endswith(a) for a in publish_actions):
|
if not any(f.stem.endswith(a) for a in publish_actions):
|
||||||
print(f'{str(f)} not in publish actions anymore. Removing...')
|
print(f"{str(f)} not in publish actions anymore. Removing...")
|
||||||
f.unlink()
|
f.unlink()
|
||||||
|
|
||||||
# Set Scene
|
# Set Scene
|
||||||
@@ -70,7 +76,7 @@ def render_preview(directory, asset_catalog, render_actions, publish_actions, re
|
|||||||
scn.tool_settings.use_keyframe_insert_auto = False
|
scn.tool_settings.use_keyframe_insert_auto = False
|
||||||
|
|
||||||
# Render Setting
|
# Render Setting
|
||||||
rnd.engine = 'BLENDER_EEVEE'
|
rnd.engine = "BLENDER_EEVEE"
|
||||||
rnd.use_simplify = False
|
rnd.use_simplify = False
|
||||||
rnd.use_stamp_date = True
|
rnd.use_stamp_date = True
|
||||||
rnd.use_stamp_time = True
|
rnd.use_stamp_time = True
|
||||||
@@ -89,7 +95,7 @@ def render_preview(directory, asset_catalog, render_actions, publish_actions, re
|
|||||||
rnd.use_stamp = True
|
rnd.use_stamp = True
|
||||||
rnd.stamp_font_size = 16
|
rnd.stamp_font_size = 16
|
||||||
rnd.use_stamp_labels = False
|
rnd.use_stamp_labels = False
|
||||||
rnd.image_settings.file_format = 'JPEG'
|
rnd.image_settings.file_format = "JPEG"
|
||||||
|
|
||||||
# Viewport Look
|
# Viewport Look
|
||||||
# ----------
|
# ----------
|
||||||
@@ -108,94 +114,104 @@ def render_preview(directory, asset_catalog, render_actions, publish_actions, re
|
|||||||
"""
|
"""
|
||||||
# Cycles Mat Shading
|
# Cycles Mat Shading
|
||||||
for a in bpy.context.screen.areas:
|
for a in bpy.context.screen.areas:
|
||||||
if a.type == 'VIEW_3D':
|
if a.type == "VIEW_3D":
|
||||||
a.spaces[0].overlay.show_overlays = False
|
a.spaces[0].overlay.show_overlays = False
|
||||||
a.spaces[0].region_3d.view_perspective = 'CAMERA'
|
a.spaces[0].region_3d.view_perspective = "CAMERA"
|
||||||
a.spaces[0].shading.show_cavity = True
|
a.spaces[0].shading.show_cavity = True
|
||||||
a.spaces[0].shading.cavity_type = 'WORLD'
|
a.spaces[0].shading.cavity_type = "WORLD"
|
||||||
a.spaces[0].shading.cavity_ridge_factor = 0.75
|
a.spaces[0].shading.cavity_ridge_factor = 0.75
|
||||||
a.spaces[0].shading.cavity_valley_factor = 1.0
|
a.spaces[0].shading.cavity_valley_factor = 1.0
|
||||||
|
|
||||||
|
|
||||||
# Add Subsurf
|
# Add Subsurf
|
||||||
# -----------
|
# -----------
|
||||||
deform_ob = [m.object for o in scn.objects \
|
deform_ob = [
|
||||||
for m in o.modifiers if m.type == 'MESH_DEFORM'
|
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 \
|
deform_ob += [
|
||||||
for m in o.modifiers if m.type == 'SURFACE_DEFORM'
|
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'
|
objects = [
|
||||||
and o not in deform_ob and o not in bpy.context.scene.collection.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:
|
for o in objects:
|
||||||
subsurf = False
|
subsurf = False
|
||||||
for m in o.modifiers:
|
for m in o.modifiers:
|
||||||
if m.type == 'SUBSURF':
|
if m.type == "SUBSURF":
|
||||||
m.show_viewport = m.show_render
|
m.show_viewport = m.show_render
|
||||||
m.levels = m.render_levels
|
m.levels = m.render_levels
|
||||||
subsurf = True
|
subsurf = True
|
||||||
break
|
break
|
||||||
|
|
||||||
if not subsurf:
|
if not subsurf:
|
||||||
subsurf = o.modifiers.new('', 'SUBSURF')
|
subsurf = o.modifiers.new("", "SUBSURF")
|
||||||
subsurf.show_viewport = subsurf.show_render
|
subsurf.show_viewport = subsurf.show_render
|
||||||
subsurf.levels = subsurf.render_levels
|
subsurf.levels = subsurf.render_levels
|
||||||
|
|
||||||
|
|
||||||
# Loop through action and render
|
# Loop through action and render
|
||||||
# ------------------------------
|
# ------------------------------
|
||||||
rig = next((o for o in scn.objects if o.type == 'ARMATURE'), None)
|
rig = next((o for o in scn.objects if o.type == "ARMATURE"), None)
|
||||||
# actions = [a for a in bpy.data.actions if a.asset_data]
|
# actions = [a for a in bpy.data.actions if a.asset_data]
|
||||||
|
|
||||||
|
|
||||||
rig.animation_data_create()
|
rig.animation_data_create()
|
||||||
for action_name in render_actions:
|
for action_name in render_actions:
|
||||||
action = bpy.data.actions.get(action_name)
|
action = bpy.data.actions.get(action_name)
|
||||||
|
|
||||||
if not action:
|
if not action:
|
||||||
print(f'\'{action_name}\' not found.')
|
print(f"'{action_name}' not found.")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
print(f"-- Current --: {action.name}")
|
print(f"-- Current --: {action.name}")
|
||||||
|
|
||||||
rnd.stamp_note_text = '{type} : {pose_name}'
|
rnd.stamp_note_text = "{type} : {pose_name}"
|
||||||
action_data = action.asset_data
|
action_data = action.asset_data
|
||||||
|
|
||||||
if 'camera' not in action_data.keys():
|
if "camera" not in action_data.keys():
|
||||||
report.append(f"'{action.name}' has no CameraData.")
|
report.append(f"'{action.name}' has no CameraData.")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
catalog_name = next((v['name'] for v in asset_catalog_data.values() if action_data.catalog_id == v['id']), None)
|
catalog_name = next(
|
||||||
pose_name = '/'.join([*catalog_name.split('-'), action.name])
|
(
|
||||||
filename = bpy.path.clean_name(f'{catalog_name}_{action.name}')
|
v["name"]
|
||||||
ext = 'jpg'
|
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
|
rig.animation_data.action = None
|
||||||
bpy.context.view_layer.update()
|
bpy.context.view_layer.update()
|
||||||
for b in rig.pose.bones:
|
for b in rig.pose.bones:
|
||||||
if re.match('^[A-Z]+\.', b.name):
|
if re.match("^[A-Z]+\.", b.name):
|
||||||
continue
|
continue
|
||||||
reset_bone(b)
|
reset_bone(b)
|
||||||
|
|
||||||
rest_pose = None
|
rest_pose = None
|
||||||
if isinstance(action.asset_data.get('rest_pose'), str):
|
if isinstance(action.asset_data.get("rest_pose"), str):
|
||||||
rest_pose = bpy.data.actions.get(action.asset_data['rest_pose'])
|
rest_pose = bpy.data.actions.get(action.asset_data["rest_pose"])
|
||||||
|
|
||||||
rig.animation_data.action = rest_pose
|
rig.animation_data.action = rest_pose
|
||||||
bpy.context.view_layer.update()
|
bpy.context.view_layer.update()
|
||||||
|
|
||||||
rig.animation_data.action = action
|
rig.animation_data.action = action
|
||||||
|
|
||||||
if 'camera' in action.asset_data.keys():
|
if "camera" in action.asset_data.keys():
|
||||||
action_cam = bpy.data.objects.get(action.asset_data['camera'], '')
|
action_cam = bpy.data.objects.get(action.asset_data["camera"], "")
|
||||||
if action_cam:
|
if action_cam:
|
||||||
scn.camera = action_cam
|
scn.camera = action_cam
|
||||||
|
|
||||||
# Is Anim
|
# Is Anim
|
||||||
if not action_data['is_single_frame'] or 'anim' in action_data.tags.keys():
|
if not action_data["is_single_frame"] or "anim" in action_data.tags.keys():
|
||||||
keyframes = get_keyframes(action)
|
keyframes = get_keyframes(action)
|
||||||
if not keyframes:
|
if not keyframes:
|
||||||
continue
|
continue
|
||||||
@@ -203,57 +219,65 @@ def render_preview(directory, asset_catalog, render_actions, publish_actions, re
|
|||||||
anim_end = keyframes[-1]
|
anim_end = keyframes[-1]
|
||||||
|
|
||||||
if anim_start < scn.frame_start:
|
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
|
continue
|
||||||
|
|
||||||
scn.frame_preview_start = anim_start
|
scn.frame_preview_start = anim_start
|
||||||
scn.frame_preview_end = anim_end
|
scn.frame_preview_end = anim_end
|
||||||
|
|
||||||
rnd.stamp_note_text = rnd.stamp_note_text.format(
|
rnd.stamp_note_text = rnd.stamp_note_text.format(
|
||||||
type='ANIM',
|
type="ANIM",
|
||||||
pose_name=pose_name,
|
pose_name=pose_name,
|
||||||
)
|
)
|
||||||
|
|
||||||
rnd.filepath = f'{str(anim_render_dir)}/{filename}_####.{ext}'
|
rnd.filepath = f"{str(anim_render_dir)}/{filename}_####.{ext}"
|
||||||
|
|
||||||
bpy.ops.render.opengl(animation=True)
|
bpy.ops.render.opengl(animation=True)
|
||||||
|
|
||||||
ffmpeg_cmd = [
|
ffmpeg_cmd = [
|
||||||
'ffmpeg', '-y',
|
"ffmpeg",
|
||||||
'-start_number', f'{anim_start:04d}',
|
"-y",
|
||||||
'-i', rnd.filepath.replace('####', '%04d'),
|
"-start_number",
|
||||||
'-c:v', 'libx264',
|
f"{anim_start:04d}",
|
||||||
str((preview_render_dir/'anim'/filename).with_suffix('.mov')),
|
"-i",
|
||||||
|
rnd.filepath.replace("####", "%04d"),
|
||||||
|
"-c:v",
|
||||||
|
"libx264",
|
||||||
|
str((preview_render_dir / "anim" / filename).with_suffix(".mov")),
|
||||||
]
|
]
|
||||||
subprocess.call(ffmpeg_cmd)
|
subprocess.call(ffmpeg_cmd)
|
||||||
|
|
||||||
# Is Pose
|
# Is Pose
|
||||||
elif action_data['is_single_frame'] or 'pose' in action_data.tags.keys():
|
elif action_data["is_single_frame"] or "pose" in action_data.tags.keys():
|
||||||
scn.frame_preview_start = scn.frame_preview_end = scn.frame_start
|
scn.frame_preview_start = scn.frame_preview_end = scn.frame_start
|
||||||
|
|
||||||
rnd.stamp_note_text = rnd.stamp_note_text.format(
|
rnd.stamp_note_text = rnd.stamp_note_text.format(
|
||||||
type='POSE',
|
type="POSE",
|
||||||
pose_name=pose_name,
|
pose_name=pose_name,
|
||||||
)
|
)
|
||||||
|
|
||||||
rnd.filepath = f'{str(preview_render_dir)}/pose/{filename}_####.{ext}'
|
rnd.filepath = f"{str(preview_render_dir)}/pose/{filename}_####.{ext}"
|
||||||
|
|
||||||
bpy.ops.render.opengl(animation=True)
|
bpy.ops.render.opengl(animation=True)
|
||||||
|
|
||||||
filename = rnd.filepath.replace('####', f'{scn.frame_preview_end:04d}')
|
filename = rnd.filepath.replace("####", f"{scn.frame_preview_end:04d}")
|
||||||
Path(filename).rename(re.sub('_[0-9]{4}.', '.', filename))
|
Path(filename).rename(re.sub("_[0-9]{4}.", ".", filename))
|
||||||
|
|
||||||
shutil.rmtree(anim_render_dir)
|
shutil.rmtree(anim_render_dir)
|
||||||
|
|
||||||
# Report
|
# Report
|
||||||
# ------
|
# ------
|
||||||
if report:
|
if report:
|
||||||
report_file = blendfile.parent / Path(f'{blendfile.stem}report').with_suffix('.txt')
|
report_file = blendfile.parent / Path(f"{blendfile.stem}report").with_suffix(
|
||||||
|
".txt"
|
||||||
|
)
|
||||||
if not report_file.exists():
|
if not report_file.exists():
|
||||||
report_file.touch(exist_ok=False)
|
report_file.touch(exist_ok=False)
|
||||||
|
|
||||||
report_file.write_text('-')
|
report_file.write_text("-")
|
||||||
report_file.write_text('\n'.join(report))
|
report_file.write_text("\n".join(report))
|
||||||
|
|
||||||
result = report_file
|
result = report_file
|
||||||
|
|
||||||
@@ -262,31 +286,40 @@ def render_preview(directory, asset_catalog, render_actions, publish_actions, re
|
|||||||
|
|
||||||
open_file(result)
|
open_file(result)
|
||||||
|
|
||||||
files = [str(f) for f in sorted((preview_render_dir/'pose').glob('*.jpg'))]
|
files = [str(f) for f in sorted((preview_render_dir / "pose").glob("*.jpg"))]
|
||||||
|
|
||||||
mosaic_export(
|
mosaic_export(
|
||||||
files=files, catalog_data=asset_catalog_data,
|
files=files,
|
||||||
row=2, columns=2, auto_calculate=True,
|
catalog_data=asset_catalog_data,
|
||||||
bg_color=(0.18, 0.18, 0.18,), resize_output=100
|
row=2,
|
||||||
|
columns=2,
|
||||||
|
auto_calculate=True,
|
||||||
|
bg_color=(
|
||||||
|
0.18,
|
||||||
|
0.18,
|
||||||
|
0.18,
|
||||||
|
),
|
||||||
|
resize_output=100,
|
||||||
)
|
)
|
||||||
|
|
||||||
bpy.ops.wm.quit_blender()
|
bpy.ops.wm.quit_blender()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Add Comment To the tracker",
|
||||||
|
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
||||||
|
)
|
||||||
|
|
||||||
if __name__ == '__main__' :
|
parser.add_argument("--directory")
|
||||||
parser = argparse.ArgumentParser(description='Add Comment To the tracker',
|
parser.add_argument("--asset-catalog")
|
||||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
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')
|
if "--" in sys.argv:
|
||||||
parser.add_argument('--asset-catalog')
|
index = sys.argv.index("--")
|
||||||
parser.add_argument('--render-actions', nargs='+')
|
sys.argv = [sys.argv[index - 1], *sys.argv[index + 1 :]]
|
||||||
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:]]
|
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
render_preview(**vars(args))
|
render_preview(**vars(args))
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import argparse
|
||||||
|
import bpy
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# sys.path.append(str(Path(__file__).parents[3]))
|
||||||
|
from asset_library.common.bl_utils import (
|
||||||
|
get_preview,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def clear_asset(action_name="", use_fake_user=False):
|
||||||
|
|
||||||
|
scn = bpy.context.scene
|
||||||
|
|
||||||
|
action = bpy.data.actions.get(action_name)
|
||||||
|
if not action:
|
||||||
|
print(f"No {action_name} not found.")
|
||||||
|
bpy.ops.wm.quit_blender()
|
||||||
|
|
||||||
|
action.asset_clear()
|
||||||
|
if use_fake_user:
|
||||||
|
action.use_fake_user = True
|
||||||
|
else:
|
||||||
|
preview = get_preview(asset_path=bpy.data.filepath, asset_name=action_name)
|
||||||
|
if preview:
|
||||||
|
preview.unlink()
|
||||||
|
bpy.data.actions.remove(action)
|
||||||
|
|
||||||
|
bpy.ops.wm.save_mainfile(filepath=bpy.data.filepath, compress=True, exit=True)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Add Comment To the tracker",
|
||||||
|
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument("--action-name")
|
||||||
|
parser.add_argument("--use-fake-user", type=json.loads, default="false")
|
||||||
|
|
||||||
|
if "--" in sys.argv:
|
||||||
|
index = sys.argv.index("--")
|
||||||
|
sys.argv = [sys.argv[index - 1], *sys.argv[index + 1 :]]
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
clear_asset(**vars(args))
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
import math
|
import math
|
||||||
import numpy as np
|
import numpy as np
|
||||||
@@ -11,23 +10,25 @@ def alpha_to_color(pixels_data, color):
|
|||||||
new_pixels_data = []
|
new_pixels_data = []
|
||||||
for i in pixels_data:
|
for i in pixels_data:
|
||||||
height, width, array_d = i.shape
|
height, width, array_d = i.shape
|
||||||
mask = i[:,:,3:]
|
mask = i[:, :, 3:]
|
||||||
background = np.array([color[0], color[1], color[2] ,1], dtype=np.float32)
|
background = np.array([color[0], color[1], color[2], 1], dtype=np.float32)
|
||||||
background = np.tile(background, (height*width))
|
background = np.tile(background, (height * width))
|
||||||
background = np.reshape(background, (height,width,4))
|
background = np.reshape(background, (height, width, 4))
|
||||||
new_pixels_data.append(i * mask + background * (1 - mask))
|
new_pixels_data.append(i * mask + background * (1 - mask))
|
||||||
# print(new_pixels_data)#Dbg
|
# print(new_pixels_data)#Dbg
|
||||||
return new_pixels_data
|
return new_pixels_data
|
||||||
|
|
||||||
|
|
||||||
def create_array(height, width):
|
def create_array(height, width):
|
||||||
return np.zeros((height*width*4), dtype=np.float32)
|
return np.zeros((height * width * 4), dtype=np.float32)
|
||||||
|
|
||||||
|
|
||||||
def read_pixels_data(img, source_height, source_width):
|
def read_pixels_data(img, source_height, source_width):
|
||||||
img_w, img_h = img.size
|
img_w, img_h = img.size
|
||||||
|
|
||||||
if img_w != source_width :
|
if img_w != source_width:
|
||||||
scale = abs(img_w/source_width)
|
scale = abs(img_w / source_width)
|
||||||
img.scale(int(img_w/scale), int(img_h/scale))
|
img.scale(int(img_w / scale), int(img_h / scale))
|
||||||
img_w, img_h = img.size
|
img_w, img_h = img.size
|
||||||
|
|
||||||
array = create_array(img_h, img_w)
|
array = create_array(img_h, img_w)
|
||||||
@@ -35,46 +36,59 @@ def read_pixels_data(img, source_height, source_width):
|
|||||||
array = array.reshape(img_h, img_w, 4)
|
array = array.reshape(img_h, img_w, 4)
|
||||||
|
|
||||||
if array.shape[0] != source_height:
|
if array.shape[0] != source_height:
|
||||||
#print('ARRAY SHAPE', array.shape[:], source_height)
|
# print('ARRAY SHAPE', array.shape[:], source_height)
|
||||||
missing_height = int(abs(source_height-img_h)/2)
|
missing_height = int(abs(source_height - img_h) / 2)
|
||||||
empty_array = create_array(missing_height, source_width)
|
empty_array = create_array(missing_height, source_width)
|
||||||
empty_array = empty_array.reshape(missing_height, source_width, 4)
|
empty_array = empty_array.reshape(missing_height, source_width, 4)
|
||||||
array = np.vstack((empty_array, array, empty_array))
|
array = np.vstack((empty_array, array, empty_array))
|
||||||
|
|
||||||
return array.reshape(source_height, source_width, 4)
|
return array.reshape(source_height, source_width, 4)
|
||||||
|
|
||||||
|
|
||||||
def create_final(output_name, pixels_data, final_height, final_width):
|
def create_final(output_name, pixels_data, final_height, final_width):
|
||||||
#print('output_name: ', output_name)
|
# print('output_name: ', output_name)
|
||||||
|
|
||||||
new_img = bpy.data.images.get(output_name)
|
new_img = bpy.data.images.get(output_name)
|
||||||
if new_img:
|
if new_img:
|
||||||
bpy.data.images.remove(new_img)
|
bpy.data.images.remove(new_img)
|
||||||
|
|
||||||
new_img = bpy.data.images.new(output_name, final_width, final_height)
|
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)
|
new_img.pixels.foreach_set(pixels_data)
|
||||||
|
|
||||||
return new_img
|
return new_img
|
||||||
|
|
||||||
|
|
||||||
def guess_input_format(img_list):
|
def guess_input_format(img_list):
|
||||||
for i in img_list:
|
for i in img_list:
|
||||||
if i.size[0] == i.size[1]:
|
if i.size[0] == i.size[1]:
|
||||||
return i.size
|
return i.size
|
||||||
|
|
||||||
|
|
||||||
def format_files(files, catalog_data):
|
def format_files(files, catalog_data):
|
||||||
img_dict = {}
|
img_dict = {}
|
||||||
for k, v in catalog_data.items():
|
for k, v in catalog_data.items():
|
||||||
if '/' not in k:
|
if "/" not in k:
|
||||||
continue
|
continue
|
||||||
img_dict[v['name']] = [f for f in files if v['name'] in f]
|
img_dict[v["name"]] = [f for f in files if v["name"] in f]
|
||||||
|
|
||||||
return img_dict
|
return img_dict
|
||||||
|
|
||||||
|
|
||||||
def mosaic_export(
|
def mosaic_export(
|
||||||
files, catalog_data, row=2, columns=2, auto_calculate=True,
|
files,
|
||||||
bg_color=(0.18, 0.18, 0.18,), resize_output=100,
|
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)
|
img_dict = format_files(files, catalog_data)
|
||||||
@@ -92,21 +106,21 @@ def mosaic_export(
|
|||||||
chars = Path(files_list[0]).parts[-4]
|
chars = Path(files_list[0]).parts[-4]
|
||||||
output_dir = str(Path(files_list[0]).parent.parent)
|
output_dir = str(Path(files_list[0]).parent.parent)
|
||||||
|
|
||||||
ext = 'jpg'
|
ext = "jpg"
|
||||||
output_name = f'{chars}_{cat}.{ext}'
|
output_name = f"{chars}_{cat}.{ext}"
|
||||||
|
|
||||||
for img in files_list:
|
for img in files_list:
|
||||||
img_list.append(bpy.data.images.load(img, check_existing=True))
|
img_list.append(bpy.data.images.load(img, check_existing=True))
|
||||||
|
|
||||||
for i in img_list:
|
for i in img_list:
|
||||||
i.colorspace_settings.name = 'Raw'
|
i.colorspace_settings.name = "Raw"
|
||||||
|
|
||||||
if auto_calculate:
|
if auto_calculate:
|
||||||
rows = int(math.sqrt(len(img_list)))
|
rows = int(math.sqrt(len(img_list)))
|
||||||
columns = math.ceil(len(img_list)/rows)
|
columns = math.ceil(len(img_list) / rows)
|
||||||
|
|
||||||
if rows*columns < len(img_list):
|
if rows * columns < len(img_list):
|
||||||
raise AttributeError('Grid too small for number of images')
|
raise AttributeError("Grid too small for number of images")
|
||||||
|
|
||||||
src_w, src_h = img_list[0].size
|
src_w, src_h = img_list[0].size
|
||||||
final_w = src_w * columns
|
final_w = src_w * columns
|
||||||
@@ -114,33 +128,36 @@ def mosaic_export(
|
|||||||
|
|
||||||
img_pixels = [read_pixels_data(img, src_h, src_w) for img in img_list]
|
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
|
##It not, create empty array
|
||||||
h_stack = []
|
h_stack = []
|
||||||
total_len = rows*columns
|
total_len = rows * columns
|
||||||
if len(img_pixels) < total_len:
|
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.append(create_array(src_h, src_w).reshape(src_h, src_w, 4))
|
||||||
|
|
||||||
img_pixels = alpha_to_color(img_pixels, bg_color)
|
img_pixels = alpha_to_color(img_pixels, bg_color)
|
||||||
for i in range(0,len(img_pixels),columns):
|
for i in range(0, len(img_pixels), columns):
|
||||||
h_stack.append(np.hstack(img_pixels[i:i+columns]))
|
h_stack.append(np.hstack(img_pixels[i : i + columns]))
|
||||||
if rows > 1:
|
if rows > 1:
|
||||||
combined_stack = np.vstack(h_stack[::-1])
|
combined_stack = np.vstack(h_stack[::-1])
|
||||||
else:
|
else:
|
||||||
combined_stack = np.hstack((h_stack[:]))
|
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:
|
if resize_output != 100:
|
||||||
w, h = combined_img.size
|
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.filepath_raw = '/'.join([output_dir, output_name])
|
combined_img.file_format = "JPEG"
|
||||||
combined_img.file_format = 'JPEG'
|
|
||||||
combined_img.save()
|
combined_img.save()
|
||||||
|
|
||||||
print(f"""
|
print(
|
||||||
|
f"""
|
||||||
Image saved: {combined_img.filepath_raw}
|
Image saved: {combined_img.filepath_raw}
|
||||||
""")
|
"""
|
||||||
|
)
|
||||||
@@ -13,15 +13,7 @@ import functools
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
from bpy.types import (
|
from bpy.types import Action, Bone, Context, FCurve, Keyframe, Object, TimelineMarker
|
||||||
Action,
|
|
||||||
Bone,
|
|
||||||
Context,
|
|
||||||
FCurve,
|
|
||||||
Keyframe,
|
|
||||||
Object,
|
|
||||||
TimelineMarker
|
|
||||||
)
|
|
||||||
|
|
||||||
from asset_library.common.bl_utils import active_catalog_id, split_path
|
from asset_library.common.bl_utils import active_catalog_id, split_path
|
||||||
|
|
||||||
@@ -30,6 +22,7 @@ FCurveValue = Union[float, int]
|
|||||||
pose_bone_re = re.compile(r'pose.bones\["([^"]+)"\]')
|
pose_bone_re = re.compile(r'pose.bones\["([^"]+)"\]')
|
||||||
"""RegExp for matching FCurve data paths."""
|
"""RegExp for matching FCurve data paths."""
|
||||||
|
|
||||||
|
|
||||||
def is_pose(action):
|
def is_pose(action):
|
||||||
for fc in action.fcurves:
|
for fc in action.fcurves:
|
||||||
if len(fc.keyframe_points) > 1:
|
if len(fc.keyframe_points) > 1:
|
||||||
@@ -37,6 +30,7 @@ def is_pose(action):
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def get_bone_visibility(data_path):
|
def get_bone_visibility(data_path):
|
||||||
bone, prop = split_path(data_path)
|
bone, prop = split_path(data_path)
|
||||||
|
|
||||||
@@ -47,6 +41,7 @@ def get_bone_visibility(data_path):
|
|||||||
|
|
||||||
return ob.data.layers[b_layers[0]]
|
return ob.data.layers[b_layers[0]]
|
||||||
|
|
||||||
|
|
||||||
def get_keyframes(action, selected=False, includes=[]):
|
def get_keyframes(action, selected=False, includes=[]):
|
||||||
if selected:
|
if selected:
|
||||||
# keyframes = sorted([int(k.co[0]) for f in action.fcurves for k in f.keyframe_points if k.select_control_point and get_bone_visibility(f.data_path)])
|
# keyframes = sorted([int(k.co[0]) for f in action.fcurves for k in f.keyframe_points if k.select_control_point and get_bone_visibility(f.data_path)])
|
||||||
@@ -65,14 +60,21 @@ def get_keyframes(action, selected=False, includes=[]):
|
|||||||
if len(keyframes) <= 1:
|
if len(keyframes) <= 1:
|
||||||
keyframes = [bpy.context.scene.frame_current]
|
keyframes = [bpy.context.scene.frame_current]
|
||||||
else:
|
else:
|
||||||
keyframes = sorted([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
|
return keyframes
|
||||||
|
|
||||||
|
|
||||||
def get_marker(action):
|
def get_marker(action):
|
||||||
if action.pose_markers:
|
if action.pose_markers:
|
||||||
markers = action.pose_markers
|
markers = action.pose_markers
|
||||||
return next((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):
|
def reset_bone(bone, transform=True, custom_props=True):
|
||||||
if transform:
|
if transform:
|
||||||
@@ -95,29 +97,32 @@ def reset_bone(bone, transform=True, custom_props=True):
|
|||||||
|
|
||||||
if not isinstance(value, (int, float)) or not id_prop:
|
if not isinstance(value, (int, float)) or not id_prop:
|
||||||
continue
|
continue
|
||||||
bone[key] = id_prop.as_dict()['default']
|
bone[key] = id_prop.as_dict()["default"]
|
||||||
|
|
||||||
|
|
||||||
def is_asset_action(action):
|
def is_asset_action(action):
|
||||||
return action.asset_data and action.asset_data.catalog_id != str(uuid.UUID(int=0))
|
return action.asset_data and action.asset_data.catalog_id != str(uuid.UUID(int=0))
|
||||||
|
|
||||||
|
|
||||||
def conform_action(action):
|
def conform_action(action):
|
||||||
tags = ('pose', 'anim')
|
tags = ("pose", "anim")
|
||||||
|
|
||||||
if any(tag in action.asset_data.tags.keys() for tag in tags):
|
if any(tag in action.asset_data.tags.keys() for tag in tags):
|
||||||
return
|
return
|
||||||
|
|
||||||
for fc in action.fcurves:
|
for fc in action.fcurves:
|
||||||
action.asset_data['is_single_frame'] = True
|
action.asset_data["is_single_frame"] = True
|
||||||
if len(fc.keyframe_points) > 1:
|
if len(fc.keyframe_points) > 1:
|
||||||
action.asset_data['is_single_frame'] = False
|
action.asset_data["is_single_frame"] = False
|
||||||
break
|
break
|
||||||
|
|
||||||
if action.asset_data['is_single_frame']:
|
if action.asset_data["is_single_frame"]:
|
||||||
action.asset_data.tags.new('pose')
|
action.asset_data.tags.new("pose")
|
||||||
else:
|
else:
|
||||||
action.asset_data.tags.new('anim')
|
action.asset_data.tags.new("anim")
|
||||||
|
|
||||||
def clean_action(action='', frame_start=0, frame_end=0, excludes=[], includes=[]):
|
|
||||||
|
def clean_action(action="", frame_start=0, frame_end=0, excludes=[], includes=[]):
|
||||||
## Clean Keyframe Before/After Range
|
## Clean Keyframe Before/After Range
|
||||||
for fc in action.fcurves:
|
for fc in action.fcurves:
|
||||||
bone, prop = split_path(fc.data_path)
|
bone, prop = split_path(fc.data_path)
|
||||||
@@ -134,18 +139,20 @@ def clean_action(action='', frame_start=0, frame_end=0, excludes=[], includes=[]
|
|||||||
|
|
||||||
# Remove Keyframe out of range
|
# Remove Keyframe out of range
|
||||||
for k in reversed(fc.keyframe_points):
|
for k in reversed(fc.keyframe_points):
|
||||||
if int(k.co[0]) not in range(frame_start, frame_end+1):
|
if int(k.co[0]) not in range(frame_start, frame_end + 1):
|
||||||
fc.keyframe_points.remove(k)
|
fc.keyframe_points.remove(k)
|
||||||
fc.update()
|
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):
|
with bpy.data.libraries.load(str(action_path), link=False) as (data_from, data_to):
|
||||||
data_to.actions = [action_name]
|
data_to.actions = [action_name]
|
||||||
|
|
||||||
return data_to.actions[0]
|
return data_to.actions[0]
|
||||||
|
|
||||||
|
|
||||||
def apply_anim(action_lib, ob, bones=[]):
|
def apply_anim(action_lib, ob, bones=[]):
|
||||||
from mathutils import Vector
|
from mathutils import Vector
|
||||||
|
|
||||||
@@ -162,14 +169,23 @@ def apply_anim(action_lib, ob, bones=[]):
|
|||||||
|
|
||||||
keys = sorted([k.co[0] for f in action_lib.fcurves for k in f.keyframe_points])
|
keys = sorted([k.co[0] for f in action_lib.fcurves for k in f.keyframe_points])
|
||||||
if not keys:
|
if not keys:
|
||||||
print(f'The action {action_lib.name} has no keyframes')
|
print(f"The action {action_lib.name} has no keyframes")
|
||||||
return
|
return
|
||||||
|
|
||||||
first_key = keys[0]
|
first_key = keys[0]
|
||||||
key_offset = scn.frame_current - first_key
|
key_offset = scn.frame_current - first_key
|
||||||
|
|
||||||
key_attr = ('type', 'interpolation', 'handle_left_type', 'handle_right_type',
|
key_attr = (
|
||||||
'amplitude', 'back', 'easing', 'period', 'handle_right', 'handle_left'
|
"type",
|
||||||
|
"interpolation",
|
||||||
|
"handle_left_type",
|
||||||
|
"handle_right_type",
|
||||||
|
"amplitude",
|
||||||
|
"back",
|
||||||
|
"easing",
|
||||||
|
"period",
|
||||||
|
"handle_right",
|
||||||
|
"handle_left",
|
||||||
)
|
)
|
||||||
for fc in action_lib.fcurves:
|
for fc in action_lib.fcurves:
|
||||||
bone_name, prop_name = split_path(fc.data_path)
|
bone_name, prop_name = split_path(fc.data_path)
|
||||||
@@ -182,17 +198,16 @@ def apply_anim(action_lib, ob, bones=[]):
|
|||||||
action_fc = action.fcurves.new(
|
action_fc = action.fcurves.new(
|
||||||
fc.data_path,
|
fc.data_path,
|
||||||
index=fc.array_index,
|
index=fc.array_index,
|
||||||
action_group=fc.group.name if fc.group else fc.data_path.split('"')[1]
|
action_group=fc.group.name if fc.group else fc.data_path.split('"')[1],
|
||||||
)
|
)
|
||||||
|
|
||||||
for kf_lib in fc.keyframe_points:
|
for kf_lib in fc.keyframe_points:
|
||||||
kf = action_fc.keyframe_points.insert(
|
kf = action_fc.keyframe_points.insert(
|
||||||
frame=kf_lib.co[0] + key_offset,
|
frame=kf_lib.co[0] + key_offset, value=kf_lib.co[1]
|
||||||
value=kf_lib.co[1]
|
|
||||||
)
|
)
|
||||||
for attr in key_attr:
|
for attr in key_attr:
|
||||||
src_val = getattr(kf_lib, attr)
|
src_val = getattr(kf_lib, attr)
|
||||||
if attr.startswith('handle') and 'type' not in attr:
|
if attr.startswith("handle") and "type" not in attr:
|
||||||
src_val += Vector((key_offset, 0))
|
src_val += Vector((key_offset, 0))
|
||||||
|
|
||||||
setattr(kf, attr, src_val)
|
setattr(kf, attr, src_val)
|
||||||
@@ -203,5 +218,5 @@ def apply_anim(action_lib, ob, bones=[]):
|
|||||||
for window in bpy.context.window_manager.windows:
|
for window in bpy.context.window_manager.windows:
|
||||||
screen = window.screen
|
screen = window.screen
|
||||||
for area in screen.areas:
|
for area in screen.areas:
|
||||||
if area.type == 'GRAPH_EDITOR':
|
if area.type == "GRAPH_EDITOR":
|
||||||
area.tag_redraw()
|
area.tag_redraw()
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import bpy
|
||||||
|
|
||||||
|
|
||||||
|
def draw_context_menu(layout):
|
||||||
|
params = bpy.context.space_data.params
|
||||||
|
asset = bpy.context.asset_file_handle
|
||||||
|
|
||||||
|
layout.operator(
|
||||||
|
"assetlib.open_blend", text="Open blend file"
|
||||||
|
) # .asset = asset.name
|
||||||
|
layout.operator("assetlib.play_preview", text="Play Preview")
|
||||||
|
|
||||||
|
layout.separator()
|
||||||
|
|
||||||
|
layout.operator_context = "INVOKE_DEFAULT"
|
||||||
|
|
||||||
|
# layout.operator("assetlib.rename_asset", text="Rename Action")
|
||||||
|
layout.operator("assetlib.remove_assets", text="Remove Assets")
|
||||||
|
layout.operator("assetlib.edit_data", text="Edit Asset data")
|
||||||
|
|
||||||
|
# layout.operator("actionlib.clear_asset", text="Clear Asset (Fake User)").use_fake_user = True
|
||||||
|
|
||||||
|
layout.separator()
|
||||||
|
|
||||||
|
layout.operator("actionlib.apply_selected_action", text="Apply Pose").flipped = (
|
||||||
|
False
|
||||||
|
)
|
||||||
|
layout.operator(
|
||||||
|
"actionlib.apply_selected_action", text="Apply Pose (Flipped)"
|
||||||
|
).flipped = True
|
||||||
|
|
||||||
|
layout.separator()
|
||||||
|
|
||||||
|
layout.operator(
|
||||||
|
"poselib.blend_pose_asset_for_keymap", text="Blend Pose"
|
||||||
|
).flipped = False
|
||||||
|
layout.operator(
|
||||||
|
"poselib.blend_pose_asset_for_keymap", text="Blend Pose (Flipped)"
|
||||||
|
).flipped = True
|
||||||
|
|
||||||
|
layout.separator()
|
||||||
|
|
||||||
|
layout.operator(
|
||||||
|
"poselib.pose_asset_select_bones", text="Select Bones"
|
||||||
|
).selected_side = "CURRENT"
|
||||||
|
layout.operator(
|
||||||
|
"poselib.pose_asset_select_bones", text="Select Bones (Flipped)"
|
||||||
|
).selected_side = "FLIPPED"
|
||||||
|
layout.operator(
|
||||||
|
"poselib.pose_asset_select_bones", text="Select Bones (Both)"
|
||||||
|
).selected_side = "BOTH"
|
||||||
|
|
||||||
|
layout.separator()
|
||||||
|
# layout.operator("asset.library_refresh")
|
||||||
|
if params.display_type == "THUMBNAIL":
|
||||||
|
layout.prop_menu_enum(params, "display_size")
|
||||||
|
|
||||||
|
|
||||||
|
def draw_header(layout):
|
||||||
|
"""Draw the header of the Asset Browser Window"""
|
||||||
|
|
||||||
|
layout.separator()
|
||||||
|
layout.operator("actionlib.store_anim_pose", text="Add Action", icon="FILE_NEW")
|
||||||
@@ -1,11 +1,10 @@
|
|||||||
|
|
||||||
|
|
||||||
from typing import List, Tuple
|
from typing import List, Tuple
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
|
|
||||||
addon_keymaps: List[Tuple[bpy.types.KeyMap, bpy.types.KeyMapItem]] = []
|
addon_keymaps: List[Tuple[bpy.types.KeyMap, bpy.types.KeyMapItem]] = []
|
||||||
|
|
||||||
|
|
||||||
def register():
|
def register():
|
||||||
wm = bpy.context.window_manager
|
wm = bpy.context.window_manager
|
||||||
addon = wm.keyconfigs.addon
|
addon = wm.keyconfigs.addon
|
||||||
@@ -15,34 +14,49 @@ def register():
|
|||||||
km = addon.keymaps.new(name="File Browser Main", space_type="FILE_BROWSER")
|
km = addon.keymaps.new(name="File Browser Main", space_type="FILE_BROWSER")
|
||||||
|
|
||||||
# DblClick to apply pose.
|
# DblClick to apply pose.
|
||||||
kmi = km.keymap_items.new("actionlib.apply_selected_action", "LEFTMOUSE", "DOUBLE_CLICK")
|
kmi = km.keymap_items.new(
|
||||||
|
"actionlib.apply_selected_action", "LEFTMOUSE", "DOUBLE_CLICK"
|
||||||
|
)
|
||||||
kmi.properties.flipped = False
|
kmi.properties.flipped = False
|
||||||
addon_keymaps.append((km, kmi))
|
addon_keymaps.append((km, kmi))
|
||||||
|
|
||||||
kmi = km.keymap_items.new("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
|
kmi.properties.flipped = True
|
||||||
addon_keymaps.append((km, kmi))
|
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
|
kmi.properties.flipped = False
|
||||||
addon_keymaps.append((km, kmi))
|
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
|
kmi.properties.flipped = True
|
||||||
addon_keymaps.append((km, kmi))
|
addon_keymaps.append((km, kmi))
|
||||||
|
|
||||||
kmi = km.keymap_items.new("poselib.pose_asset_select_bones", "S", "PRESS")
|
kmi = km.keymap_items.new("poselib.pose_asset_select_bones", "S", "PRESS")
|
||||||
kmi.properties.selected_side = 'CURRENT'
|
kmi.properties.selected_side = "CURRENT"
|
||||||
addon_keymaps.append((km, kmi))
|
addon_keymaps.append((km, kmi))
|
||||||
|
|
||||||
kmi = km.keymap_items.new("poselib.pose_asset_select_bones", "S", "PRESS", alt=True)
|
kmi = km.keymap_items.new("poselib.pose_asset_select_bones", "S", "PRESS", alt=True)
|
||||||
kmi.properties.selected_side = 'FLIPPED'
|
kmi.properties.selected_side = "FLIPPED"
|
||||||
addon_keymaps.append((km, kmi))
|
addon_keymaps.append((km, kmi))
|
||||||
|
|
||||||
kmi = km.keymap_items.new("poselib.pose_asset_select_bones", "S", "PRESS", alt=True, ctrl=True)
|
kmi = km.keymap_items.new(
|
||||||
kmi.properties.selected_side = 'BOTH'
|
"poselib.pose_asset_select_bones", "S", "PRESS", alt=True, ctrl=True
|
||||||
|
)
|
||||||
|
kmi.properties.selected_side = "BOTH"
|
||||||
addon_keymaps.append((km, kmi))
|
addon_keymaps.append((km, kmi))
|
||||||
|
|
||||||
|
|
||||||
def unregister():
|
def unregister():
|
||||||
for km, kmi in addon_keymaps:
|
for km, kmi in addon_keymaps:
|
||||||
km.keymap_items.remove(kmi)
|
km.keymap_items.remove(kmi)
|
||||||
@@ -24,50 +24,60 @@ from functools import partial
|
|||||||
from pprint import pprint
|
from pprint import pprint
|
||||||
|
|
||||||
|
|
||||||
from asset_library.pose.pose_creation import(
|
from asset_library.pose.pose_creation import (
|
||||||
create_pose_asset_from_context,
|
create_pose_asset_from_context,
|
||||||
assign_from_asset_browser
|
assign_from_asset_browser,
|
||||||
)
|
)
|
||||||
|
|
||||||
from asset_library.pose.pose_usage import(
|
from asset_library.pose.pose_usage import select_bones, flip_side_name
|
||||||
select_bones,
|
|
||||||
flip_side_name
|
|
||||||
)
|
|
||||||
|
|
||||||
from asset_library.data_type.action.functions import(
|
from asset_library.action.functions import (
|
||||||
apply_anim,
|
apply_anim,
|
||||||
append_action,
|
append_action,
|
||||||
clean_action,
|
clean_action,
|
||||||
reset_bone,
|
reset_bone,
|
||||||
is_asset_action,
|
is_asset_action,
|
||||||
conform_action
|
conform_action,
|
||||||
)
|
)
|
||||||
|
|
||||||
from bpy.props import (BoolProperty, CollectionProperty, EnumProperty,
|
from bpy.props import (
|
||||||
PointerProperty, StringProperty, IntProperty)
|
BoolProperty,
|
||||||
|
CollectionProperty,
|
||||||
|
EnumProperty,
|
||||||
|
PointerProperty,
|
||||||
|
StringProperty,
|
||||||
|
IntProperty,
|
||||||
|
)
|
||||||
|
|
||||||
from bpy.types import (Action, Context, Event, FileSelectEntry, Object,
|
from bpy.types import (
|
||||||
Operator, PropertyGroup)
|
Action,
|
||||||
|
Context,
|
||||||
|
Event,
|
||||||
|
FileSelectEntry,
|
||||||
|
Object,
|
||||||
|
Operator,
|
||||||
|
PropertyGroup,
|
||||||
|
)
|
||||||
|
|
||||||
from bpy_extras import asset_utils
|
from bpy_extras import asset_utils
|
||||||
from bpy_extras.io_utils import ExportHelper, ImportHelper
|
from bpy_extras.io_utils import ExportHelper, ImportHelper
|
||||||
|
|
||||||
from asset_library.data_type.action.functions import (
|
from asset_library.action.functions import (
|
||||||
is_pose,
|
is_pose,
|
||||||
get_marker,
|
get_marker,
|
||||||
get_keyframes,
|
get_keyframes,
|
||||||
)
|
)
|
||||||
|
|
||||||
from asset_library.common.functions import (
|
from asset_library.common.functions import (
|
||||||
#get_actionlib_dir,
|
# get_actionlib_dir,
|
||||||
#get_asset_source,
|
# get_asset_source,
|
||||||
#get_catalog_path,
|
# get_catalog_path,
|
||||||
#read_catalog,
|
# read_catalog,
|
||||||
#set_actionlib_dir,
|
# set_actionlib_dir,
|
||||||
#resync_lib,
|
# resync_lib,
|
||||||
get_active_library,
|
get_active_library,
|
||||||
get_active_catalog,
|
get_active_catalog,
|
||||||
asset_warning_callback
|
asset_warning_callback,
|
||||||
)
|
)
|
||||||
|
|
||||||
from asset_library.common.bl_utils import (
|
from asset_library.common.bl_utils import (
|
||||||
@@ -79,10 +89,10 @@ from asset_library.common.bl_utils import (
|
|||||||
get_preview,
|
get_preview,
|
||||||
get_view3d_persp,
|
get_view3d_persp,
|
||||||
get_viewport,
|
get_viewport,
|
||||||
#load_assets_from,
|
# load_assets_from,
|
||||||
get_asset_space_params,
|
get_asset_space_params,
|
||||||
get_bl_cmd,
|
get_bl_cmd,
|
||||||
get_overriden_col
|
get_overriden_col,
|
||||||
)
|
)
|
||||||
|
|
||||||
from asset_library.common.file_utils import (
|
from asset_library.common.file_utils import (
|
||||||
@@ -124,17 +134,17 @@ class ACTIONLIB_OT_restore_previous_action(Operator):
|
|||||||
self._timer = wm.event_timer_add(0.001, window=context.window)
|
self._timer = wm.event_timer_add(0.001, window=context.window)
|
||||||
wm.modal_handler_add(self)
|
wm.modal_handler_add(self)
|
||||||
|
|
||||||
return {'RUNNING_MODAL'}
|
return {"RUNNING_MODAL"}
|
||||||
|
|
||||||
def modal(self, context, event):
|
def modal(self, context, event):
|
||||||
if event.type != 'TIMER':
|
if event.type != "TIMER":
|
||||||
return {'RUNNING_MODAL'}
|
return {"RUNNING_MODAL"}
|
||||||
|
|
||||||
wm = context.window_manager
|
wm = context.window_manager
|
||||||
wm.event_timer_remove(self._timer)
|
wm.event_timer_remove(self._timer)
|
||||||
|
|
||||||
context.object.pose.apply_pose_from_action(self.pose_action)
|
context.object.pose.apply_pose_from_action(self.pose_action)
|
||||||
return {'FINISHED'}
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
class ACTIONLIB_OT_assign_action(Operator):
|
class ACTIONLIB_OT_assign_action(Operator):
|
||||||
@@ -158,8 +168,8 @@ class ACTIONLIB_OT_assign_action(Operator):
|
|||||||
class ACTIONLIB_OT_replace_pose(Operator):
|
class ACTIONLIB_OT_replace_pose(Operator):
|
||||||
bl_idname = "actionlib.replace_pose"
|
bl_idname = "actionlib.replace_pose"
|
||||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||||
bl_label = 'Update Pose'
|
bl_label = "Update Pose"
|
||||||
bl_description = 'Update selected Pose. ! Works only on Pose, not Anim !'
|
bl_description = "Update selected Pose. ! Works only on Pose, not Anim !"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context: Context) -> bool:
|
def poll(cls, context: Context) -> bool:
|
||||||
@@ -170,23 +180,28 @@ class ACTIONLIB_OT_replace_pose(Operator):
|
|||||||
# else:
|
# else:
|
||||||
# cls.poll_message_set(f"Current Action {context.id.name} different than Edit Action {wm.edit_pose_action}")
|
# cls.poll_message_set(f"Current Action {context.id.name} different than Edit Action {wm.edit_pose_action}")
|
||||||
# return False
|
# return False
|
||||||
if context.mode == 'POSE' and context.area.ui_type == 'ASSETS' and context.active_file:
|
if (
|
||||||
|
context.mode == "POSE"
|
||||||
|
and context.area.ui_type == "ASSETS"
|
||||||
|
and context.active_file
|
||||||
|
):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def execute(self, context: Context) -> Set[str]:
|
def execute(self, context: Context) -> Set[str]:
|
||||||
wm = context.window_manager
|
wm = context.window_manager
|
||||||
active = context.active_file
|
active = context.active_file
|
||||||
#print('active: ', active)
|
# print('active: ', active)
|
||||||
|
|
||||||
select_bones(
|
select_bones(
|
||||||
context.object,
|
context.object,
|
||||||
context.asset_file_handle.local_id,
|
context.asset_file_handle.local_id,
|
||||||
selected_side='BOTH',
|
selected_side="BOTH",
|
||||||
toggle=False)
|
toggle=False,
|
||||||
|
)
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
'name':active.name,
|
"name": active.name,
|
||||||
'catalog_id':active.asset_data.catalog_id,
|
"catalog_id": active.asset_data.catalog_id,
|
||||||
}
|
}
|
||||||
data.update(dict(active.asset_data))
|
data.update(dict(active.asset_data))
|
||||||
|
|
||||||
@@ -195,11 +210,11 @@ class ACTIONLIB_OT_replace_pose(Operator):
|
|||||||
# if 'is_single_frame' in asset_data.keys():
|
# if 'is_single_frame' in asset_data.keys():
|
||||||
# data.update({'is_single_frame' : active.asset_data['is_single_frame']})
|
# data.update({'is_single_frame' : active.asset_data['is_single_frame']})
|
||||||
|
|
||||||
#print('data: ', data)
|
# print('data: ', data)
|
||||||
|
|
||||||
action = create_pose_asset_from_context(
|
action = create_pose_asset_from_context(
|
||||||
context,
|
context,
|
||||||
data['name'],
|
data["name"],
|
||||||
)
|
)
|
||||||
if not action:
|
if not action:
|
||||||
self.report( # type: ignore
|
self.report( # type: ignore
|
||||||
@@ -213,33 +228,36 @@ class ACTIONLIB_OT_replace_pose(Operator):
|
|||||||
_old_action.use_fake_user = False
|
_old_action.use_fake_user = False
|
||||||
bpy.data.actions.remove(_old_action)
|
bpy.data.actions.remove(_old_action)
|
||||||
|
|
||||||
action.name = data['name']
|
action.name = data["name"]
|
||||||
action.asset_data.catalog_id = data['catalog_id']
|
action.asset_data.catalog_id = data["catalog_id"]
|
||||||
|
|
||||||
for k, v in data.items():
|
for k, v in data.items():
|
||||||
if k in ('camera', 'is_single_frame'):
|
if k in ("camera", "is_single_frame"):
|
||||||
action.asset_data[k] = v
|
action.asset_data[k] = v
|
||||||
|
|
||||||
if not is_pose(action) and 'pose' in action.asset_data.tags.keys() and 'anim' not in action.asset_data.tags.keys():
|
if (
|
||||||
|
not is_pose(action)
|
||||||
|
and "pose" in action.asset_data.tags.keys()
|
||||||
|
and "anim" not in action.asset_data.tags.keys()
|
||||||
|
):
|
||||||
for tag in action.asset_data.tags:
|
for tag in action.asset_data.tags:
|
||||||
if tag != 'pose':
|
if tag != "pose":
|
||||||
continue
|
continue
|
||||||
action.asset_data.tags.remove(tag)
|
action.asset_data.tags.remove(tag)
|
||||||
action.asset_data.tags.new('anim')
|
action.asset_data.tags.new("anim")
|
||||||
|
|
||||||
|
return {"FINISHED"}
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
|
|
||||||
class ACTIONLIB_OT_apply_anim(Operator):
|
class ACTIONLIB_OT_apply_anim(Operator):
|
||||||
bl_idname = "actionlib.apply_anim"
|
bl_idname = "actionlib.apply_anim"
|
||||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||||
bl_label = 'Apply Anim'
|
bl_label = "Apply Anim"
|
||||||
bl_description = 'Apply selected Anim to selected bones'
|
bl_description = "Apply selected Anim to selected bones"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context: Context) -> bool:
|
def poll(cls, context: Context) -> bool:
|
||||||
return context.mode == 'POSE'
|
return context.mode == "POSE"
|
||||||
|
|
||||||
def execute(self, context: Context) -> Set[str]:
|
def execute(self, context: Context) -> Set[str]:
|
||||||
ob = context.object
|
ob = context.object
|
||||||
@@ -247,23 +265,23 @@ class ACTIONLIB_OT_apply_anim(Operator):
|
|||||||
to_remove = False
|
to_remove = False
|
||||||
|
|
||||||
prefs = get_addon_prefs()
|
prefs = get_addon_prefs()
|
||||||
#params = get_asset_space_params(context.area)
|
# params = get_asset_space_params(context.area)
|
||||||
asset_library_ref = context.asset_library_ref
|
asset_library_ref = context.asset_library_ref
|
||||||
|
|
||||||
if asset_library_ref == 'LOCAL':
|
if asset_library_ref == "LOCAL":
|
||||||
action = bpy.data.actions[active_action.name]
|
action = bpy.data.actions[active_action.name]
|
||||||
to_remove = False
|
to_remove = False
|
||||||
else:
|
else:
|
||||||
asset_file_handle = bpy.context.asset_file_handle
|
asset_file_handle = bpy.context.asset_file_handle
|
||||||
if asset_file_handle is None:
|
if asset_file_handle is None:
|
||||||
return {'CANCELLED'}
|
return {"CANCELLED"}
|
||||||
|
|
||||||
if asset_file_handle.local_id:
|
if asset_file_handle.local_id:
|
||||||
return {'CANCELLED'}
|
return {"CANCELLED"}
|
||||||
|
|
||||||
lib = get_active_library()
|
lib = get_active_library()
|
||||||
if 'filepath' in asset_file_handle.asset_data:
|
if "filepath" in asset_file_handle.asset_data:
|
||||||
action_path = asset_file_handle.asset_data['filepath']
|
action_path = asset_file_handle.asset_data["filepath"]
|
||||||
action_path = lib.library_type.format_path(action_path)
|
action_path = lib.library_type.format_path(action_path)
|
||||||
else:
|
else:
|
||||||
action_path = bpy.types.AssetHandle.get_full_library_path(
|
action_path = bpy.types.AssetHandle.get_full_library_path(
|
||||||
@@ -277,7 +295,9 @@ class ACTIONLIB_OT_apply_anim(Operator):
|
|||||||
)
|
)
|
||||||
to_remove = True
|
to_remove = True
|
||||||
else:
|
else:
|
||||||
self.report({"WARNING"}, f"Could not load action path {action_path} not exist")
|
self.report(
|
||||||
|
{"WARNING"}, f"Could not load action path {action_path} not exist"
|
||||||
|
)
|
||||||
return {"CANCELLED"}
|
return {"CANCELLED"}
|
||||||
|
|
||||||
bones = [b.name for b in context.selected_pose_bones_from_active_object]
|
bones = [b.name for b in context.selected_pose_bones_from_active_object]
|
||||||
@@ -285,7 +305,7 @@ class ACTIONLIB_OT_apply_anim(Operator):
|
|||||||
if to_remove:
|
if to_remove:
|
||||||
bpy.data.actions.remove(action)
|
bpy.data.actions.remove(action)
|
||||||
|
|
||||||
return {'FINISHED'}
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@@ -464,8 +484,10 @@ class ACTIONLIB_OT_create_anim_asset(Operator):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
params = get_asset_space_params(asset_browse_area)
|
params = get_asset_space_params(asset_browse_area)
|
||||||
if params.asset_library_ref != 'LOCAL':
|
if params.asset_library_ref != "LOCAL":
|
||||||
cls.poll_message_set("Asset Browser must be set to the Current File library")
|
cls.poll_message_set(
|
||||||
|
"Asset Browser must be set to the Current File library"
|
||||||
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
@@ -477,7 +499,7 @@ class ACTIONLIB_OT_create_anim_asset(Operator):
|
|||||||
action.asset_generate_preview()
|
action.asset_generate_preview()
|
||||||
|
|
||||||
data = action.asset_data
|
data = action.asset_data
|
||||||
#data.catalog_id = str(uuid.UUID(int=0))
|
# data.catalog_id = str(uuid.UUID(int=0))
|
||||||
asset_browse_area: Optional[bpy.types.Area] = area_from_context(context)
|
asset_browse_area: Optional[bpy.types.Area] = area_from_context(context)
|
||||||
asset_space_params = params(asset_browse_area)
|
asset_space_params = params(asset_browse_area)
|
||||||
|
|
||||||
@@ -485,67 +507,71 @@ class ACTIONLIB_OT_create_anim_asset(Operator):
|
|||||||
|
|
||||||
data_dict = dict(
|
data_dict = dict(
|
||||||
is_single_frame=False,
|
is_single_frame=False,
|
||||||
camera= context.scene.camera.name if context.scene.camera else '',
|
camera=context.scene.camera.name if context.scene.camera else "",
|
||||||
)
|
)
|
||||||
data.tags.new('anim')
|
data.tags.new("anim")
|
||||||
|
|
||||||
for k, v in data_dict.items():
|
for k, v in data_dict.items():
|
||||||
data[k] = v
|
data[k] = v
|
||||||
###
|
###
|
||||||
|
|
||||||
return {'FINISHED'}
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
class ACTIONLIB_OT_apply_selected_action(Operator):
|
class ACTIONLIB_OT_apply_selected_action(Operator):
|
||||||
bl_idname = "actionlib.apply_selected_action"
|
bl_idname = "actionlib.apply_selected_action"
|
||||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||||
bl_label = 'Apply Pose/Anim'
|
bl_label = "Apply Pose/Anim"
|
||||||
bl_description = 'Apply selected Action to selected bones'
|
bl_description = "Apply selected Action to selected bones"
|
||||||
|
|
||||||
flipped: BoolProperty(name="Flipped", default=False) # type: ignore
|
flipped: BoolProperty(name="Flipped", default=False) # type: ignore
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context: Context) -> bool:
|
def poll(cls, context: Context) -> bool:
|
||||||
if context.mode == 'POSE' and context.area.ui_type == 'ASSETS':
|
if context.mode == "POSE" and context.area.ui_type == "ASSETS":
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def execute(self, context: Context) -> Set[str]:
|
def execute(self, context: Context) -> Set[str]:
|
||||||
active_action = context.active_file
|
active_action = context.active_file
|
||||||
|
|
||||||
if 'pose' in active_action.asset_data.tags.keys():
|
if "pose" in active_action.asset_data.tags.keys():
|
||||||
bpy.ops.poselib.apply_pose_asset_for_keymap(flipped=self.flipped)
|
bpy.ops.poselib.apply_pose_asset_for_keymap(flipped=self.flipped)
|
||||||
else:
|
else:
|
||||||
bpy.ops.actionlib.apply_anim()
|
bpy.ops.actionlib.apply_anim()
|
||||||
|
|
||||||
return {'FINISHED'}
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
class ACTIONLIB_OT_edit_action(Operator):
|
class ACTIONLIB_OT_edit_action(Operator):
|
||||||
bl_idname = "actionlib.edit_action"
|
bl_idname = "actionlib.edit_action"
|
||||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||||
bl_label = 'Edit Action'
|
bl_label = "Edit Action"
|
||||||
bl_description = 'Assign active action and set Camera'
|
bl_description = "Assign active action and set Camera"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context: Context) -> bool:
|
def poll(cls, context: Context) -> bool:
|
||||||
if context.mode == 'POSE' and context.area.ui_type == 'ASSETS' and context.active_file:
|
if (
|
||||||
|
context.mode == "POSE"
|
||||||
|
and context.area.ui_type == "ASSETS"
|
||||||
|
and context.active_file
|
||||||
|
):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def execute(self, context: Context) -> Set[str]:
|
def execute(self, context: Context) -> Set[str]:
|
||||||
scn = context.scene
|
scn = context.scene
|
||||||
rest_pose = bpy.data.actions.get(context.id.asset_data.get('rest_pose', ''))
|
rest_pose = bpy.data.actions.get(context.id.asset_data.get("rest_pose", ""))
|
||||||
|
|
||||||
context.object.animation_data_create()
|
context.object.animation_data_create()
|
||||||
keyframes = get_keyframes(context.id)
|
keyframes = get_keyframes(context.id)
|
||||||
if not keyframes:
|
if not keyframes:
|
||||||
self.report({'ERROR'}, f'No Keyframes found for {context.id.name}.')
|
self.report({"ERROR"}, f"No Keyframes found for {context.id.name}.")
|
||||||
return
|
return
|
||||||
scn.frame_set(keyframes[0])
|
scn.frame_set(keyframes[0])
|
||||||
|
|
||||||
context.object.animation_data.action = None
|
context.object.animation_data.action = None
|
||||||
|
|
||||||
for b in context.object.pose.bones:
|
for b in context.object.pose.bones:
|
||||||
if re.match('^[A-Z]+\.', b.name):
|
if re.match("^[A-Z]+\.", b.name):
|
||||||
continue
|
continue
|
||||||
reset_bone(b)
|
reset_bone(b)
|
||||||
|
|
||||||
@@ -553,8 +579,8 @@ class ACTIONLIB_OT_edit_action(Operator):
|
|||||||
context.view_layer.update()
|
context.view_layer.update()
|
||||||
context.object.animation_data.action = context.id
|
context.object.animation_data.action = context.id
|
||||||
|
|
||||||
if 'camera' in context.id.asset_data.keys():
|
if "camera" in context.id.asset_data.keys():
|
||||||
scn.camera = bpy.data.objects[context.id.asset_data['camera']]
|
scn.camera = bpy.data.objects[context.id.asset_data["camera"]]
|
||||||
|
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
@@ -562,12 +588,16 @@ class ACTIONLIB_OT_edit_action(Operator):
|
|||||||
class ACTIONLIB_OT_clear_action(Operator):
|
class ACTIONLIB_OT_clear_action(Operator):
|
||||||
bl_idname = "actionlib.clear_action"
|
bl_idname = "actionlib.clear_action"
|
||||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||||
bl_label = 'Clear Action'
|
bl_label = "Clear Action"
|
||||||
bl_description = 'Assign active action and set Camera'
|
bl_description = "Assign active action and set Camera"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context: Context) -> bool:
|
def poll(cls, context: Context) -> bool:
|
||||||
if context.mode == 'POSE' and context.area.ui_type == 'ASSETS' and context.active_file:
|
if (
|
||||||
|
context.mode == "POSE"
|
||||||
|
and context.area.ui_type == "ASSETS"
|
||||||
|
and context.active_file
|
||||||
|
):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def execute(self, context: Context) -> Set[str]:
|
def execute(self, context: Context) -> Set[str]:
|
||||||
@@ -578,7 +608,7 @@ class ACTIONLIB_OT_clear_action(Operator):
|
|||||||
context.id.asset_generate_preview()
|
context.id.asset_generate_preview()
|
||||||
|
|
||||||
for a in context.screen.areas:
|
for a in context.screen.areas:
|
||||||
if a.type == 'DOPESHEET_EDITOR' and a.ui_type == 'DOPESHEET':
|
if a.type == "DOPESHEET_EDITOR" and a.ui_type == "DOPESHEET":
|
||||||
a.tag_redraw()
|
a.tag_redraw()
|
||||||
|
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
@@ -587,12 +617,12 @@ class ACTIONLIB_OT_clear_action(Operator):
|
|||||||
class ACTIONLIB_OT_generate_preview(Operator):
|
class ACTIONLIB_OT_generate_preview(Operator):
|
||||||
bl_idname = "actionlib.generate_preview"
|
bl_idname = "actionlib.generate_preview"
|
||||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||||
bl_label = 'Generate Preview'
|
bl_label = "Generate Preview"
|
||||||
bl_description = 'Genreate Preview for Active Action'
|
bl_description = "Genreate Preview for Active Action"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context: Context) -> bool:
|
def poll(cls, context: Context) -> bool:
|
||||||
if context.object.type == 'ARMATURE' and context.mode == 'POSE':
|
if context.object.type == "ARMATURE" and context.mode == "POSE":
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def execute(self, context: Context) -> Set[str]:
|
def execute(self, context: Context) -> Set[str]:
|
||||||
@@ -603,18 +633,18 @@ class ACTIONLIB_OT_generate_preview(Operator):
|
|||||||
actions = context.selected_files + [_action]
|
actions = context.selected_files + [_action]
|
||||||
|
|
||||||
for a in context.selected_files:
|
for a in context.selected_files:
|
||||||
rest_pose = bpy.data.actions.get(a.asset_data.get('rest_pose', ''))
|
rest_pose = bpy.data.actions.get(a.asset_data.get("rest_pose", ""))
|
||||||
if rest_pose:
|
if rest_pose:
|
||||||
context.object.animation_data.action = rest_pose
|
context.object.animation_data.action = rest_pose
|
||||||
bpy.context.view_layer.update()
|
bpy.context.view_layer.update()
|
||||||
else:
|
else:
|
||||||
context.object.animation_data.action = None
|
context.object.animation_data.action = None
|
||||||
for b in context.object.pose.bones:
|
for b in context.object.pose.bones:
|
||||||
if re.match('^[A-Z]+\.', b.name):
|
if re.match("^[A-Z]+\.", b.name):
|
||||||
continue
|
continue
|
||||||
reset_bone(b)
|
reset_bone(b)
|
||||||
|
|
||||||
a_cam = bpy.data.objects.get(a.asset_data['camera'])
|
a_cam = bpy.data.objects.get(a.asset_data["camera"])
|
||||||
if a_cam:
|
if a_cam:
|
||||||
context.scene.camera = a_cam
|
context.scene.camera = a_cam
|
||||||
bpy.context.view_layer.update()
|
bpy.context.view_layer.update()
|
||||||
@@ -631,15 +661,15 @@ class ACTIONLIB_OT_generate_preview(Operator):
|
|||||||
class ACTIONLIB_OT_update_action_data(Operator):
|
class ACTIONLIB_OT_update_action_data(Operator):
|
||||||
bl_idname = "actionlib.update_action_data"
|
bl_idname = "actionlib.update_action_data"
|
||||||
bl_options = {"REGISTER", "INTERNAL"}
|
bl_options = {"REGISTER", "INTERNAL"}
|
||||||
bl_label = 'Udpate Action Data'
|
bl_label = "Udpate Action Data"
|
||||||
bl_description = 'Update Action Metadata'
|
bl_description = "Update Action Metadata"
|
||||||
|
|
||||||
tags: EnumProperty(
|
tags: EnumProperty(
|
||||||
name='Tags',
|
name="Tags",
|
||||||
items=(
|
items=(
|
||||||
('POSE', "pose", ""),
|
("POSE", "pose", ""),
|
||||||
('ANIM', "anim", ""),
|
("ANIM", "anim", ""),
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
use_tags: BoolProperty(default=False)
|
use_tags: BoolProperty(default=False)
|
||||||
@@ -675,7 +705,7 @@ class ACTIONLIB_OT_update_action_data(Operator):
|
|||||||
row.prop(self, "use_camera", text="")
|
row.prop(self, "use_camera", text="")
|
||||||
sub = row.row()
|
sub = row.row()
|
||||||
sub.active = self.use_camera
|
sub.active = self.use_camera
|
||||||
sub.prop(scn.actionlib, "camera", text="", icon='CAMERA_DATA')
|
sub.prop(scn.actionlib, "camera", text="", icon="CAMERA_DATA")
|
||||||
|
|
||||||
heading = layout.column(align=True, heading="Rest Pose")
|
heading = layout.column(align=True, heading="Rest Pose")
|
||||||
row = heading.row(align=True)
|
row = heading.row(align=True)
|
||||||
@@ -689,11 +719,11 @@ class ACTIONLIB_OT_update_action_data(Operator):
|
|||||||
|
|
||||||
for action in context.selected_files:
|
for action in context.selected_files:
|
||||||
if self.use_camera:
|
if self.use_camera:
|
||||||
action.asset_data['camera'] = context.scene.actionlib.camera.name
|
action.asset_data["camera"] = context.scene.actionlib.camera.name
|
||||||
|
|
||||||
if self.use_tags:
|
if self.use_tags:
|
||||||
tag = self.tags.lower()
|
tag = self.tags.lower()
|
||||||
not_tag = 'anim' if tag == 'pose' else 'pose'
|
not_tag = "anim" if tag == "pose" else "pose"
|
||||||
|
|
||||||
if tag not in action.asset_data.tags.keys():
|
if tag not in action.asset_data.tags.keys():
|
||||||
if not_tag in action.asset_data.tags.keys():
|
if not_tag in action.asset_data.tags.keys():
|
||||||
@@ -704,29 +734,27 @@ class ACTIONLIB_OT_update_action_data(Operator):
|
|||||||
|
|
||||||
action.asset_data.tags.new(tag)
|
action.asset_data.tags.new(tag)
|
||||||
|
|
||||||
if 'pose' in action.asset_data.tags.keys():
|
if "pose" in action.asset_data.tags.keys():
|
||||||
action.asset_data['is_single_frame'] = True
|
action.asset_data["is_single_frame"] = True
|
||||||
else:
|
else:
|
||||||
action.asset_data['is_single_frame'] = False
|
action.asset_data["is_single_frame"] = False
|
||||||
|
|
||||||
|
|
||||||
if self.use_rest_pose:
|
if self.use_rest_pose:
|
||||||
name = scn.actionlib.rest_pose.name if scn.actionlib.rest_pose else ''
|
name = scn.actionlib.rest_pose.name if scn.actionlib.rest_pose else ""
|
||||||
action.asset_data['rest_pose'] = name
|
action.asset_data["rest_pose"] = name
|
||||||
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
class ACTIONLIB_OT_assign_rest_pose(Operator):
|
class ACTIONLIB_OT_assign_rest_pose(Operator):
|
||||||
bl_idname = "actionlib.mark_as_rest_pose"
|
bl_idname = "actionlib.mark_as_rest_pose"
|
||||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||||
bl_label = 'Mark As Rest Pose'
|
bl_label = "Mark As Rest Pose"
|
||||||
bl_description = 'Mark Pose as Rest Pose'
|
bl_description = "Mark Pose as Rest Pose"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context: Context) -> bool:
|
def poll(cls, context: Context) -> bool:
|
||||||
if context.area.ui_type == 'ASSETS':
|
if context.area.ui_type == "ASSETS":
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def execute(self, context: Context) -> Set[str]:
|
def execute(self, context: Context) -> Set[str]:
|
||||||
@@ -734,16 +762,16 @@ class ACTIONLIB_OT_assign_rest_pose(Operator):
|
|||||||
context.scene.actionlib.rest_pose = context.id
|
context.scene.actionlib.rest_pose = context.id
|
||||||
|
|
||||||
print(f"'{active_action.name.title()}' marked as Rest Pose.")
|
print(f"'{active_action.name.title()}' marked as Rest Pose.")
|
||||||
return {'FINISHED'}
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
class ACTIONLIB_OT_open_blendfile(Operator):
|
class ACTIONLIB_OT_open_blendfile(Operator):
|
||||||
bl_idname = "actionlib.open_blendfile"
|
bl_idname = "actionlib.open_blendfile"
|
||||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||||
bl_label = 'Set Paths'
|
bl_label = "Set Paths"
|
||||||
bl_description = 'Open Containing File'
|
bl_description = "Open Containing File"
|
||||||
|
|
||||||
replace_local : BoolProperty(default=False)
|
replace_local: BoolProperty(default=False)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context):
|
def poll(cls, context):
|
||||||
@@ -751,7 +779,7 @@ class ACTIONLIB_OT_open_blendfile(Operator):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
sp = context.space_data
|
sp = context.space_data
|
||||||
if sp.params.asset_library_ref == 'LOCAL':
|
if sp.params.asset_library_ref == "LOCAL":
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
@@ -764,13 +792,11 @@ class ACTIONLIB_OT_open_blendfile(Operator):
|
|||||||
blendfile=str(asset_path),
|
blendfile=str(asset_path),
|
||||||
)
|
)
|
||||||
subprocess.Popen(cmd)
|
subprocess.Popen(cmd)
|
||||||
return {'FINISHED'}
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
|
# LIBRARY_ITEMS = []
|
||||||
|
"""
|
||||||
#LIBRARY_ITEMS = []
|
|
||||||
'''
|
|
||||||
def callback_operator(modal_func, operator, override={}):
|
def callback_operator(modal_func, operator, override={}):
|
||||||
|
|
||||||
def wrap(self, context, event):
|
def wrap(self, context, event):
|
||||||
@@ -782,7 +808,7 @@ def callback_operator(modal_func, operator, override={}):
|
|||||||
|
|
||||||
return retset
|
return retset
|
||||||
return wrap
|
return wrap
|
||||||
'''
|
"""
|
||||||
|
|
||||||
|
|
||||||
class ACTIONLIB_OT_make_custom_preview(Operator):
|
class ACTIONLIB_OT_make_custom_preview(Operator):
|
||||||
@@ -794,34 +820,47 @@ class ACTIONLIB_OT_make_custom_preview(Operator):
|
|||||||
prefs = get_addons_prefs()
|
prefs = get_addons_prefs()
|
||||||
|
|
||||||
if not prefs.preview_modal:
|
if not prefs.preview_modal:
|
||||||
with context.temp_override(area=self.source_area, region=self.source_area.regions[-1]):
|
with context.temp_override(
|
||||||
bpy.ops.actionlib.store_anim_pose("INVOKE_DEFAULT", clear_previews=False, **prefs.add_asset_dict)
|
area=self.source_area, region=self.source_area.regions[-1]
|
||||||
|
):
|
||||||
|
bpy.ops.actionlib.store_anim_pose(
|
||||||
|
"INVOKE_DEFAULT", clear_previews=False, **prefs.add_asset_dict
|
||||||
|
)
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
return {'PASS_THROUGH'}
|
return {"PASS_THROUGH"}
|
||||||
|
|
||||||
def invoke(self, context, event):
|
def invoke(self, context, event):
|
||||||
|
|
||||||
self.source_area = bpy.context.area
|
self.source_area = bpy.context.area
|
||||||
|
|
||||||
view3d = get_viewport()
|
view3d = get_viewport()
|
||||||
with context.temp_override(area=view3d, region=view3d.regions[-1], window=context.window):
|
with context.temp_override(
|
||||||
|
area=view3d, region=view3d.regions[-1], window=context.window
|
||||||
|
):
|
||||||
# To close the popup
|
# To close the popup
|
||||||
bpy.ops.screen.screen_full_area()
|
bpy.ops.screen.screen_full_area()
|
||||||
bpy.ops.screen.back_to_previous()
|
bpy.ops.screen.back_to_previous()
|
||||||
|
|
||||||
view3d = get_viewport()
|
view3d = get_viewport()
|
||||||
with context.temp_override(area=view3d, region=view3d.regions[-1], window=context.window):
|
with context.temp_override(
|
||||||
bpy.ops.assetlib.make_custom_preview('INVOKE_DEFAULT', modal=True)
|
area=view3d, region=view3d.regions[-1], window=context.window
|
||||||
|
):
|
||||||
|
bpy.ops.assetlib.make_custom_preview("INVOKE_DEFAULT", modal=True)
|
||||||
|
|
||||||
context.window_manager.modal_handler_add(self)
|
context.window_manager.modal_handler_add(self)
|
||||||
return {'RUNNING_MODAL'}
|
return {"RUNNING_MODAL"}
|
||||||
|
|
||||||
|
|
||||||
def get_preview_items(self, context):
|
def get_preview_items(self, context):
|
||||||
prefs = get_addon_prefs()
|
prefs = get_addon_prefs()
|
||||||
return sorted([(k, k, '', v.icon_id, index) for index, (k, v) in enumerate(prefs.previews.items())], reverse=True)
|
return sorted(
|
||||||
|
[
|
||||||
|
(k, k, "", v.icon_id, index)
|
||||||
|
for index, (k, v) in enumerate(prefs.previews.items())
|
||||||
|
],
|
||||||
|
reverse=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ACTIONLIB_OT_store_anim_pose(Operator):
|
class ACTIONLIB_OT_store_anim_pose(Operator):
|
||||||
@@ -829,32 +868,40 @@ class ACTIONLIB_OT_store_anim_pose(Operator):
|
|||||||
bl_label = "Add Action to the current library"
|
bl_label = "Add Action to the current library"
|
||||||
bl_description = "Store current pose/anim to local library"
|
bl_description = "Store current pose/anim to local library"
|
||||||
|
|
||||||
warning: StringProperty(name='')
|
warning: StringProperty(name="")
|
||||||
path: StringProperty(name='Path')
|
path: StringProperty(name="Path")
|
||||||
catalog: StringProperty(name='Catalog', update=asset_warning_callback, options={'TEXTEDIT_UPDATE'})
|
catalog: StringProperty(
|
||||||
name: StringProperty(name='Name', update=asset_warning_callback, options={'TEXTEDIT_UPDATE'})
|
name="Catalog", update=asset_warning_callback, options={"TEXTEDIT_UPDATE"}
|
||||||
action_type: EnumProperty(name='Type', items=[('POSE', 'Pose', ''), ('ANIMATION', 'Animation', '')])
|
)
|
||||||
|
name: StringProperty(
|
||||||
|
name="Name", update=asset_warning_callback, options={"TEXTEDIT_UPDATE"}
|
||||||
|
)
|
||||||
|
action_type: EnumProperty(
|
||||||
|
name="Type", items=[("POSE", "Pose", ""), ("ANIMATION", "Animation", "")]
|
||||||
|
)
|
||||||
frame_start: IntProperty(name="Frame Start")
|
frame_start: IntProperty(name="Frame Start")
|
||||||
frame_end: IntProperty(name="Frame End")
|
frame_end: IntProperty(name="Frame End")
|
||||||
tags: StringProperty(name='Tags', description='Tags need to separate with a comma (,)')
|
tags: StringProperty(
|
||||||
description: StringProperty(name='Description')
|
name="Tags", description="Tags need to separate with a comma (,)"
|
||||||
preview : EnumProperty(items=get_preview_items)
|
)
|
||||||
clear_previews : BoolProperty(default=True)
|
description: StringProperty(name="Description")
|
||||||
store_library: StringProperty(name='Store Library')
|
preview: EnumProperty(items=get_preview_items)
|
||||||
|
clear_previews: BoolProperty(default=True)
|
||||||
|
store_library: StringProperty(name="Store Library")
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context: Context) -> bool:
|
def poll(cls, context: Context) -> bool:
|
||||||
ob = context.object
|
ob = context.object
|
||||||
if not ob:
|
if not ob:
|
||||||
cls.poll_message_set(f'You have no active object')
|
cls.poll_message_set(f"You have no active object")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if not ob.type == 'ARMATURE':
|
if not ob.type == "ARMATURE":
|
||||||
cls.poll_message_set(f'Active object {ob.name} is not of type ARMATURE')
|
cls.poll_message_set(f"Active object {ob.name} is not of type ARMATURE")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if not ob.animation_data or not ob.animation_data.action:
|
if not ob.animation_data or not ob.animation_data.action:
|
||||||
cls.poll_message_set(f'Active object {ob.name} has no action or keyframes')
|
cls.poll_message_set(f"Active object {ob.name} has no action or keyframes")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
@@ -869,8 +916,17 @@ class ACTIONLIB_OT_store_anim_pose(Operator):
|
|||||||
# col.operator("asset.tag_remove", icon='REMOVE', text="")
|
# col.operator("asset.tag_remove", icon='REMOVE', text="")
|
||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
keys = ("catalog", "name", "action_type", "frame_start", "frame_end", "tags", "description", "store_library")
|
keys = (
|
||||||
return {k : getattr(self, k) for k in keys}
|
"catalog",
|
||||||
|
"name",
|
||||||
|
"action_type",
|
||||||
|
"frame_start",
|
||||||
|
"frame_end",
|
||||||
|
"tags",
|
||||||
|
"description",
|
||||||
|
"store_library",
|
||||||
|
)
|
||||||
|
return {k: getattr(self, k) for k in keys}
|
||||||
|
|
||||||
def draw(self, context):
|
def draw(self, context):
|
||||||
layout = self.layout
|
layout = self.layout
|
||||||
@@ -878,57 +934,59 @@ class ACTIONLIB_OT_store_anim_pose(Operator):
|
|||||||
prefs = get_addon_prefs()
|
prefs = get_addon_prefs()
|
||||||
|
|
||||||
split = layout.split(factor=0.39, align=True)
|
split = layout.split(factor=0.39, align=True)
|
||||||
#row = split.row(align=False)
|
# row = split.row(align=False)
|
||||||
#split.use_property_split = False
|
# split.use_property_split = False
|
||||||
split.alignment = 'RIGHT'
|
split.alignment = "RIGHT"
|
||||||
|
|
||||||
split.label(text='Preview')
|
split.label(text="Preview")
|
||||||
|
|
||||||
sub = split.row(align=True)
|
sub = split.row(align=True)
|
||||||
sub.template_icon_view(self, "preview", show_labels=False)
|
sub.template_icon_view(self, "preview", show_labels=False)
|
||||||
sub.separator()
|
sub.separator()
|
||||||
sub.operator("actionlib.make_custom_preview", icon='RESTRICT_RENDER_OFF', text='')
|
sub.operator(
|
||||||
|
"actionlib.make_custom_preview", icon="RESTRICT_RENDER_OFF", text=""
|
||||||
|
)
|
||||||
|
|
||||||
prefs.add_asset_dict.clear()
|
prefs.add_asset_dict.clear()
|
||||||
prefs.add_asset_dict.update(self.to_dict())
|
prefs.add_asset_dict.update(self.to_dict())
|
||||||
|
|
||||||
sub.label(icon='BLANK1')
|
sub.label(icon="BLANK1")
|
||||||
#layout.ui_units_x = 50
|
# layout.ui_units_x = 50
|
||||||
|
|
||||||
#row = layout.row(align=True)
|
# row = layout.row(align=True)
|
||||||
layout.use_property_split = True
|
layout.use_property_split = True
|
||||||
|
|
||||||
if self.current_library.merge_libraries:
|
if self.current_library.merge_libraries:
|
||||||
layout.prop(self.current_library, 'store_library', expand=False)
|
layout.prop(self.current_library, "store_library", expand=False)
|
||||||
|
|
||||||
#row.label(text='Action Name')
|
# row.label(text='Action Name')
|
||||||
layout.prop(self, "action_type", text="Type", expand=True)
|
layout.prop(self, "action_type", text="Type", expand=True)
|
||||||
if self.action_type == 'ANIMATION':
|
if self.action_type == "ANIMATION":
|
||||||
col = layout.column(align=True)
|
col = layout.column(align=True)
|
||||||
col.prop(self, "frame_start")
|
col.prop(self, "frame_start")
|
||||||
col.prop(self, "frame_end", text='End')
|
col.prop(self, "frame_end", text="End")
|
||||||
|
|
||||||
layout.prop(self, "catalog", text="Catalog")
|
layout.prop(self, "catalog", text="Catalog")
|
||||||
layout.prop(self, "name", text="Name")
|
layout.prop(self, "name", text="Name")
|
||||||
|
|
||||||
#layout.separator()
|
# layout.separator()
|
||||||
#self.draw_tags(self.asset_action, layout)
|
# self.draw_tags(self.asset_action, layout)
|
||||||
layout.prop(self, 'tags')
|
layout.prop(self, "tags")
|
||||||
layout.prop(self, 'description', text='Description')
|
layout.prop(self, "description", text="Description")
|
||||||
#layout.prop(prefs, 'author', text='Author')
|
# layout.prop(prefs, 'author', text='Author')
|
||||||
|
|
||||||
#layout.prop()
|
# layout.prop()
|
||||||
|
|
||||||
layout.separator()
|
layout.separator()
|
||||||
col = layout.column()
|
col = layout.column()
|
||||||
col.use_property_split = False
|
col.use_property_split = False
|
||||||
#row.enabled = False
|
# row.enabled = False
|
||||||
|
|
||||||
if self.path:
|
if self.path:
|
||||||
col.label(text=self.path)
|
col.label(text=self.path)
|
||||||
|
|
||||||
if self.warning:
|
if self.warning:
|
||||||
col.label(icon='ERROR', text=self.warning)
|
col.label(icon="ERROR", text=self.warning)
|
||||||
|
|
||||||
def set_action_type(self):
|
def set_action_type(self):
|
||||||
ob = bpy.context.object
|
ob = bpy.context.object
|
||||||
@@ -940,9 +998,9 @@ class ACTIONLIB_OT_store_anim_pose(Operator):
|
|||||||
self.frame_start = min(keyframes_selected)
|
self.frame_start = min(keyframes_selected)
|
||||||
self.frame_end = max(keyframes_selected)
|
self.frame_end = max(keyframes_selected)
|
||||||
|
|
||||||
self.action_type = 'POSE'
|
self.action_type = "POSE"
|
||||||
if (self.frame_start != self.frame_end):
|
if self.frame_start != self.frame_end:
|
||||||
self.action_type = 'ANIMATION'
|
self.action_type = "ANIMATION"
|
||||||
|
|
||||||
def invoke(self, context, event):
|
def invoke(self, context, event):
|
||||||
prefs = get_addon_prefs()
|
prefs = get_addon_prefs()
|
||||||
@@ -961,15 +1019,14 @@ class ACTIONLIB_OT_store_anim_pose(Operator):
|
|||||||
self.current_library.store_library = lib.name
|
self.current_library.store_library = lib.name
|
||||||
self.store_library = lib.name
|
self.store_library = lib.name
|
||||||
|
|
||||||
#lib = self.current_library
|
# lib = self.current_library
|
||||||
self.tags = ''
|
self.tags = ""
|
||||||
|
|
||||||
|
# print(self, self.library_items)
|
||||||
#print(self, self.library_items)
|
|
||||||
catalog_item = lib.catalog.active_item
|
catalog_item = lib.catalog.active_item
|
||||||
|
|
||||||
if catalog_item:
|
if catalog_item:
|
||||||
self.catalog = catalog_item.path #get_active_catalog()
|
self.catalog = catalog_item.path # get_active_catalog()
|
||||||
|
|
||||||
self.set_action_type()
|
self.set_action_type()
|
||||||
|
|
||||||
@@ -978,7 +1035,7 @@ class ACTIONLIB_OT_store_anim_pose(Operator):
|
|||||||
|
|
||||||
view3d = get_viewport()
|
view3d = get_viewport()
|
||||||
with context.temp_override(area=view3d, region=view3d.regions[-1]):
|
with context.temp_override(area=view3d, region=view3d.regions[-1]):
|
||||||
bpy.ops.assetlib.make_custom_preview('INVOKE_DEFAULT')
|
bpy.ops.assetlib.make_custom_preview("INVOKE_DEFAULT")
|
||||||
|
|
||||||
else:
|
else:
|
||||||
preview_items = get_preview_items(self, context)
|
preview_items = get_preview_items(self, context)
|
||||||
@@ -988,7 +1045,7 @@ class ACTIONLIB_OT_store_anim_pose(Operator):
|
|||||||
return context.window_manager.invoke_props_dialog(self, width=350)
|
return context.window_manager.invoke_props_dialog(self, width=350)
|
||||||
|
|
||||||
def action_to_asset(self, action):
|
def action_to_asset(self, action):
|
||||||
#action.asset_mark()
|
# action.asset_mark()
|
||||||
prefs = get_addon_prefs()
|
prefs = get_addon_prefs()
|
||||||
action.name = self.name
|
action.name = self.name
|
||||||
action.asset_generate_preview()
|
action.asset_generate_preview()
|
||||||
@@ -999,17 +1056,17 @@ class ACTIONLIB_OT_store_anim_pose(Operator):
|
|||||||
action=action,
|
action=action,
|
||||||
frame_start=self.frame_start,
|
frame_start=self.frame_start,
|
||||||
frame_end=self.frame_end,
|
frame_end=self.frame_end,
|
||||||
excludes=['world', 'walk'],
|
excludes=["world", "walk"],
|
||||||
includes=bones,
|
includes=bones,
|
||||||
)
|
)
|
||||||
|
|
||||||
## Define Tags
|
## Define Tags
|
||||||
tags = [t.strip() for t in self.tags.split(',') if t]
|
tags = [t.strip() for t in self.tags.split(",") if t]
|
||||||
tag_range = f'f{self.frame_start}'
|
tag_range = f"f{self.frame_start}"
|
||||||
is_single_frame = True
|
is_single_frame = True
|
||||||
if self.action_type == 'ANIM':
|
if self.action_type == "ANIM":
|
||||||
is_single_frame = False
|
is_single_frame = False
|
||||||
tag_range = f'f{self.frame_start}-f{self.frame_end}'
|
tag_range = f"f{self.frame_start}-f{self.frame_end}"
|
||||||
|
|
||||||
tags.append(self.action_type)
|
tags.append(self.action_type)
|
||||||
tags.append(tag_range)
|
tags.append(tag_range)
|
||||||
@@ -1017,15 +1074,15 @@ class ACTIONLIB_OT_store_anim_pose(Operator):
|
|||||||
for tag in tags:
|
for tag in tags:
|
||||||
action.asset_data.tags.new(tag)
|
action.asset_data.tags.new(tag)
|
||||||
|
|
||||||
action.asset_data['is_single_frame'] = is_single_frame
|
action.asset_data["is_single_frame"] = is_single_frame
|
||||||
action.asset_data['rig'] = bpy.context.object.name
|
action.asset_data["rig"] = bpy.context.object.name
|
||||||
|
|
||||||
action.asset_data.description = self.description
|
action.asset_data.description = self.description
|
||||||
action.asset_data.author = prefs.author
|
action.asset_data.author = prefs.author
|
||||||
|
|
||||||
col = get_overriden_col(bpy.context.object)
|
col = get_overriden_col(bpy.context.object)
|
||||||
if col:
|
if col:
|
||||||
action.asset_data['col'] = col.name
|
action.asset_data["col"] = col.name
|
||||||
|
|
||||||
return action
|
return action
|
||||||
|
|
||||||
@@ -1037,25 +1094,25 @@ class ACTIONLIB_OT_store_anim_pose(Operator):
|
|||||||
space = area.spaces.active
|
space = area.spaces.active
|
||||||
|
|
||||||
attrs = [
|
attrs = [
|
||||||
(scn, 'use_preview_range', True),
|
(scn, "use_preview_range", True),
|
||||||
(scn, 'frame_preview_start', self.frame_start),
|
(scn, "frame_preview_start", self.frame_start),
|
||||||
(scn, 'frame_preview_end', self.frame_end),
|
(scn, "frame_preview_end", self.frame_end),
|
||||||
(scn.render, 'resolution_percentage', 100),
|
(scn.render, "resolution_percentage", 100),
|
||||||
(space.overlay, 'show_overlays', False),
|
(space.overlay, "show_overlays", False),
|
||||||
(space.region_3d, 'view_perspective', 'CAMERA'),
|
(space.region_3d, "view_perspective", "CAMERA"),
|
||||||
(scn.render, 'resolution_x', 1280),
|
(scn.render, "resolution_x", 1280),
|
||||||
(scn.render, 'resolution_y', 720),
|
(scn.render, "resolution_y", 720),
|
||||||
(scn.render.image_settings, 'file_format', 'FFMPEG'),
|
(scn.render.image_settings, "file_format", "FFMPEG"),
|
||||||
(scn.render.ffmpeg, 'format', 'QUICKTIME'),
|
(scn.render.ffmpeg, "format", "QUICKTIME"),
|
||||||
(scn.render.ffmpeg, 'codec', 'H264'),
|
(scn.render.ffmpeg, "codec", "H264"),
|
||||||
(scn.render.ffmpeg, 'ffmpeg_preset', 'GOOD'),
|
(scn.render.ffmpeg, "ffmpeg_preset", "GOOD"),
|
||||||
(scn.render.ffmpeg, 'constant_rate_factor', 'HIGH'),
|
(scn.render.ffmpeg, "constant_rate_factor", "HIGH"),
|
||||||
(scn.render.ffmpeg, 'gopsize', 12),
|
(scn.render.ffmpeg, "gopsize", 12),
|
||||||
(scn.render, 'filepath', str(video_path)),
|
(scn.render, "filepath", str(video_path)),
|
||||||
]
|
]
|
||||||
|
|
||||||
if self.action_type == "ANIMATION":
|
if self.action_type == "ANIMATION":
|
||||||
with attr_set(preview_attrs+video_attrs):
|
with attr_set(preview_attrs + video_attrs):
|
||||||
with ctx.temp_override(area=area):
|
with ctx.temp_override(area=area):
|
||||||
bpy.ops.render.opengl(animation=True)
|
bpy.ops.render.opengl(animation=True)
|
||||||
|
|
||||||
@@ -1063,8 +1120,8 @@ class ACTIONLIB_OT_store_anim_pose(Operator):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
def refresh(self, area):
|
def refresh(self, area):
|
||||||
bpy.ops.asset.library_refresh({"area": area, 'region': area.regions[3]})
|
bpy.ops.asset.library_refresh({"area": area, "region": area.regions[3]})
|
||||||
#space_data.activate_asset_by_id(asset, deferred=deferred)
|
# space_data.activate_asset_by_id(asset, deferred=deferred)
|
||||||
|
|
||||||
def execute(self, context: Context):
|
def execute(self, context: Context):
|
||||||
|
|
||||||
@@ -1081,8 +1138,12 @@ class ACTIONLIB_OT_store_anim_pose(Operator):
|
|||||||
lib_type = lib.library_type
|
lib_type = lib.library_type
|
||||||
|
|
||||||
asset_path = lib_type.get_asset_path(name=self.name, catalog=self.catalog)
|
asset_path = lib_type.get_asset_path(name=self.name, catalog=self.catalog)
|
||||||
img_path = lib_type.get_image_path(name=self.name, catalog=self.catalog, filepath=asset_path)
|
img_path = lib_type.get_image_path(
|
||||||
video_path = lib_type.get_video_path(name=self.name, catalog=self.catalog, filepath=asset_path)
|
name=self.name, catalog=self.catalog, filepath=asset_path
|
||||||
|
)
|
||||||
|
video_path = lib_type.get_video_path(
|
||||||
|
name=self.name, catalog=self.catalog, filepath=asset_path
|
||||||
|
)
|
||||||
|
|
||||||
## Copy Action
|
## Copy Action
|
||||||
current_action = ob.animation_data.action
|
current_action = ob.animation_data.action
|
||||||
@@ -1092,13 +1153,13 @@ class ACTIONLIB_OT_store_anim_pose(Operator):
|
|||||||
|
|
||||||
self.action_to_asset(asset_action)
|
self.action_to_asset(asset_action)
|
||||||
|
|
||||||
#lib_type.new_asset()
|
# lib_type.new_asset()
|
||||||
|
|
||||||
#Saving the video
|
# Saving the video
|
||||||
if self.action_type == "ANIMATION":
|
if self.action_type == "ANIMATION":
|
||||||
self.render_animation(video_path)
|
self.render_animation(video_path)
|
||||||
|
|
||||||
#Saving the preview image
|
# Saving the preview image
|
||||||
preview = prefs.previews[self.preview]
|
preview = prefs.previews[self.preview]
|
||||||
lib_type.write_preview(preview, img_path)
|
lib_type.write_preview(preview, img_path)
|
||||||
|
|
||||||
@@ -1112,10 +1173,10 @@ class ACTIONLIB_OT_store_anim_pose(Operator):
|
|||||||
asset_data = dict(lib_type.get_asset_data(asset_action), catalog=self.catalog)
|
asset_data = dict(lib_type.get_asset_data(asset_action), catalog=self.catalog)
|
||||||
asset_info = lib_type.format_asset_info([asset_data], asset_path=asset_path)
|
asset_info = lib_type.format_asset_info([asset_data], asset_path=asset_path)
|
||||||
|
|
||||||
#print('asset_info')
|
# print('asset_info')
|
||||||
#pprint(asset_info)
|
# pprint(asset_info)
|
||||||
|
|
||||||
diff = [dict(a, operation='ADD') for a in lib_type.flatten_cache([asset_info])]
|
diff = [dict(a, operation="ADD") for a in lib_type.flatten_cache([asset_info])]
|
||||||
|
|
||||||
ob.animation_data.action = current_action
|
ob.animation_data.action = current_action
|
||||||
|
|
||||||
@@ -1124,16 +1185,16 @@ class ACTIONLIB_OT_store_anim_pose(Operator):
|
|||||||
bpy.data.actions.remove(asset_action)
|
bpy.data.actions.remove(asset_action)
|
||||||
|
|
||||||
# TODO Write a proper method for this
|
# TODO Write a proper method for this
|
||||||
diff_path = Path(bpy.app.tempdir, 'diff.json')
|
diff_path = Path(bpy.app.tempdir, "diff.json")
|
||||||
|
|
||||||
#diff = [dict(a, operation='ADD') for a in [asset_info])]
|
# diff = [dict(a, operation='ADD') for a in [asset_info])]
|
||||||
diff_path.write_text(json.dumps(diff, indent=4))
|
diff_path.write_text(json.dumps(diff, indent=4))
|
||||||
|
|
||||||
bpy.ops.assetlib.bundle(name=lib.name, diff=str(diff_path), blocking=True)
|
bpy.ops.assetlib.bundle(name=lib.name, diff=str(diff_path), blocking=True)
|
||||||
|
|
||||||
#self.area.tag_redraw()
|
# self.area.tag_redraw()
|
||||||
|
|
||||||
self.report({'INFO'}, f'"{self.name}" has been added to the library.')
|
self.report({"INFO"}, f'"{self.name}" has been added to the library.')
|
||||||
|
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
@@ -1141,7 +1202,7 @@ class ACTIONLIB_OT_store_anim_pose(Operator):
|
|||||||
classes = (
|
classes = (
|
||||||
ACTIONLIB_OT_assign_action,
|
ACTIONLIB_OT_assign_action,
|
||||||
ACTIONLIB_OT_restore_previous_action,
|
ACTIONLIB_OT_restore_previous_action,
|
||||||
#ACTIONLIB_OT_publish,
|
# ACTIONLIB_OT_publish,
|
||||||
ACTIONLIB_OT_apply_anim,
|
ACTIONLIB_OT_apply_anim,
|
||||||
ACTIONLIB_OT_replace_pose,
|
ACTIONLIB_OT_replace_pose,
|
||||||
ACTIONLIB_OT_create_anim_asset,
|
ACTIONLIB_OT_create_anim_asset,
|
||||||
@@ -1152,7 +1213,7 @@ classes = (
|
|||||||
ACTIONLIB_OT_update_action_data,
|
ACTIONLIB_OT_update_action_data,
|
||||||
ACTIONLIB_OT_assign_rest_pose,
|
ACTIONLIB_OT_assign_rest_pose,
|
||||||
ACTIONLIB_OT_store_anim_pose,
|
ACTIONLIB_OT_store_anim_pose,
|
||||||
ACTIONLIB_OT_make_custom_preview
|
ACTIONLIB_OT_make_custom_preview,
|
||||||
)
|
)
|
||||||
|
|
||||||
register, unregister = bpy.utils.register_classes_factory(classes)
|
register, unregister = bpy.utils.register_classes_factory(classes)
|
||||||
@@ -2,20 +2,19 @@ import bpy
|
|||||||
from bpy.types import PropertyGroup
|
from bpy.types import PropertyGroup
|
||||||
from bpy.props import PointerProperty, StringProperty, BoolProperty
|
from bpy.props import PointerProperty, StringProperty, BoolProperty
|
||||||
|
|
||||||
|
|
||||||
class ACTIONLIB_PG_scene(PropertyGroup):
|
class ACTIONLIB_PG_scene(PropertyGroup):
|
||||||
flipped : BoolProperty(
|
flipped: BoolProperty(
|
||||||
name="Flip Pose",
|
name="Flip Pose",
|
||||||
default=False,
|
default=False,
|
||||||
)
|
)
|
||||||
previous_action : PointerProperty(type=bpy.types.Action)
|
previous_action: PointerProperty(type=bpy.types.Action)
|
||||||
publish_path : StringProperty(subtype='FILE_PATH')
|
publish_path: StringProperty(subtype="FILE_PATH")
|
||||||
camera : PointerProperty(type=bpy.types.Object, poll=lambda s, o: o.type == 'CAMERA')
|
camera: PointerProperty(type=bpy.types.Object, poll=lambda s, o: o.type == "CAMERA")
|
||||||
rest_pose : PointerProperty(type=bpy.types.Action, poll=lambda s, a: a.asset_data)
|
rest_pose: PointerProperty(type=bpy.types.Action, poll=lambda s, a: a.asset_data)
|
||||||
|
|
||||||
|
|
||||||
classes = (
|
classes = (ACTIONLIB_PG_scene,)
|
||||||
ACTIONLIB_PG_scene,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def register():
|
def register():
|
||||||
@@ -24,6 +23,7 @@ def register():
|
|||||||
|
|
||||||
bpy.types.Scene.actionlib = PointerProperty(type=ACTIONLIB_PG_scene)
|
bpy.types.Scene.actionlib = PointerProperty(type=ACTIONLIB_PG_scene)
|
||||||
|
|
||||||
|
|
||||||
def unregister():
|
def unregister():
|
||||||
try:
|
try:
|
||||||
del bpy.types.Scene.actionlib
|
del bpy.types.Scene.actionlib
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import argparse
|
||||||
|
import bpy
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# sys.path.append(str(Path(__file__).parents[3]))
|
||||||
|
from asset_library.common.bl_utils import (
|
||||||
|
get_preview,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def rename_pose(src_name="", dst_name=""):
|
||||||
|
|
||||||
|
scn = bpy.context.scene
|
||||||
|
action = bpy.data.actions.get(src_name)
|
||||||
|
if not action:
|
||||||
|
print(f"No {src_name} not found.")
|
||||||
|
bpy.ops.wm.quit_blender()
|
||||||
|
|
||||||
|
action.name = dst_name
|
||||||
|
preview = get_preview(asset_path=bpy.data.filepath, asset_name=src_name)
|
||||||
|
if preview:
|
||||||
|
preview.rename(re.sub(src_name, dst_name, str(preview)))
|
||||||
|
|
||||||
|
bpy.ops.wm.save_mainfile(filepath=bpy.data.filepath, compress=True, exit=True)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Add Comment To the tracker",
|
||||||
|
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument("--src-name")
|
||||||
|
parser.add_argument("--dst-name")
|
||||||
|
|
||||||
|
if "--" in sys.argv:
|
||||||
|
index = sys.argv.index("--")
|
||||||
|
sys.argv = [sys.argv[index - 1], *sys.argv[index + 1 :]]
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
rename_pose(**vars(args))
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
from bpy.types import PropertyGroup
|
||||||
|
|
||||||
|
|
||||||
|
class Adapter(PropertyGroup):
|
||||||
|
|
||||||
|
# def __init__(self):
|
||||||
|
name = "Base Adapter"
|
||||||
|
|
||||||
|
# library = None
|
||||||
|
def to_dict(self):
|
||||||
|
return {
|
||||||
|
p: getattr(self, p)
|
||||||
|
for p in self.bl_rna.properties.keys()
|
||||||
|
if p != "rna_type"
|
||||||
|
}
|
||||||
@@ -1,27 +1,28 @@
|
|||||||
|
from asset_library.collection import (
|
||||||
from asset_library.data_type.collection import (
|
|
||||||
gui,
|
gui,
|
||||||
operators,
|
operators,
|
||||||
keymaps,
|
keymaps,
|
||||||
#build_collection_blends,
|
# build_collection_blends,
|
||||||
#create_collection_library,
|
# create_collection_library,
|
||||||
)
|
)
|
||||||
|
|
||||||
if 'bpy' in locals():
|
if "bpy" in locals():
|
||||||
import importlib
|
import importlib
|
||||||
|
|
||||||
importlib.reload(gui)
|
importlib.reload(gui)
|
||||||
importlib.reload(operators)
|
importlib.reload(operators)
|
||||||
importlib.reload(keymaps)
|
importlib.reload(keymaps)
|
||||||
#importlib.reload(build_collection_blends)
|
# importlib.reload(build_collection_blends)
|
||||||
#importlib.reload(create_collection_library)
|
# importlib.reload(create_collection_library)
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
|
|
||||||
|
|
||||||
def register():
|
def register():
|
||||||
operators.register()
|
operators.register()
|
||||||
keymaps.register()
|
keymaps.register()
|
||||||
|
|
||||||
|
|
||||||
def unregister():
|
def unregister():
|
||||||
operators.unregister()
|
operators.unregister()
|
||||||
keymaps.unregister()
|
keymaps.unregister()
|
||||||
+37
-29
@@ -29,6 +29,7 @@ from asset_library.constants import ASSETLIB_FILENAME
|
|||||||
]
|
]
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def build_collection_blends(path, categories=None, clean=True):
|
def build_collection_blends(path, categories=None, clean=True):
|
||||||
|
|
||||||
t0 = time()
|
t0 = time()
|
||||||
@@ -43,30 +44,33 @@ def build_collection_blends(path, categories=None, clean=True):
|
|||||||
category_datas = json.loads(json_path.read_text())
|
category_datas = json.loads(json_path.read_text())
|
||||||
|
|
||||||
for category_data in category_datas:
|
for category_data in category_datas:
|
||||||
if categories and category_data['name'] not in categories:
|
if categories and category_data["name"] not in categories:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
bpy.ops.wm.read_homefile(use_empty=True)
|
bpy.ops.wm.read_homefile(use_empty=True)
|
||||||
|
|
||||||
|
# category_data = next(c for c in category_datas if c['name'] == category)
|
||||||
|
# _col_datas = category_data['children']
|
||||||
|
|
||||||
#category_data = next(c for c in category_datas if c['name'] == category)
|
cat_name = category_data["name"]
|
||||||
#_col_datas = category_data['children']
|
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
|
## re-iterate in grouped filepath
|
||||||
col_datas = sorted(category_data['children'], key=lambda x: x['filepath'])
|
col_datas = sorted(category_data["children"], key=lambda x: x["filepath"])
|
||||||
for filepath, col_data_groups in groupby(col_datas, key=lambda x: x['filepath']):
|
for filepath, col_data_groups in groupby(
|
||||||
#f = Path(f)
|
col_datas, key=lambda x: x["filepath"]
|
||||||
|
):
|
||||||
|
# f = Path(f)
|
||||||
if not Path(filepath).exists():
|
if not Path(filepath).exists():
|
||||||
print(f'Not exists: {filepath}')
|
print(f"Not exists: {filepath}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
col_data_groups = list(col_data_groups)
|
col_data_groups = list(col_data_groups)
|
||||||
|
|
||||||
col_names = [a['name'] for a in col_data_groups]
|
col_names = [a["name"] for a in col_data_groups]
|
||||||
linked_cols = load_datablocks(filepath, col_names, link=True, type='collections')
|
linked_cols = load_datablocks(
|
||||||
|
filepath, col_names, link=True, type="collections"
|
||||||
|
)
|
||||||
|
|
||||||
for i, col in enumerate(linked_cols):
|
for i, col in enumerate(linked_cols):
|
||||||
# iterate in linked collection and associated data
|
# iterate in linked collection and associated data
|
||||||
@@ -78,14 +82,14 @@ def build_collection_blends(path, categories=None, clean=True):
|
|||||||
|
|
||||||
## Directly link as collection inside a marked collection with same name
|
## Directly link as collection inside a marked collection with same name
|
||||||
marked_col = col_as_asset(col, verbose=True)
|
marked_col = col_as_asset(col, verbose=True)
|
||||||
marked_col.asset_data.description = asset_data.get('description', '')
|
marked_col.asset_data.description = asset_data.get("description", "")
|
||||||
marked_col.asset_data.catalog_id = category_data['id'] # assign catalog
|
marked_col.asset_data.catalog_id = category_data["id"] # assign catalog
|
||||||
|
|
||||||
for k, v in asset_data.get('metadata', {}).items():
|
for k, v in asset_data.get("metadata", {}).items():
|
||||||
marked_col.asset_data[k] = v
|
marked_col.asset_data[k] = v
|
||||||
|
|
||||||
## exclude collections and generate preview
|
## exclude collections and generate preview
|
||||||
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 = bpy.context.view_layer.layer_collection.children[marked_col.name]
|
||||||
vcol.exclude = True
|
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...):
|
## clear all objects (can be very long with a lot of objects...):
|
||||||
if clean:
|
if clean:
|
||||||
print('Removing links...')
|
print("Removing links...")
|
||||||
for lib in reversed(bpy.data.libraries):
|
for lib in reversed(bpy.data.libraries):
|
||||||
bpy.data.libraries.remove(lib)
|
bpy.data.libraries.remove(lib)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Créer les dossiers intermediaires
|
# Créer les dossiers intermediaires
|
||||||
build_path.parent.mkdir(parents=True, exist_ok=True)
|
build_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
print('Saving to', build_path)
|
print("Saving to", build_path)
|
||||||
bpy.ops.wm.save_as_mainfile(filepath=str(build_path), compress=False)
|
bpy.ops.wm.save_as_mainfile(filepath=str(build_path), compress=False)
|
||||||
|
|
||||||
print("build time:", f'{time() - t0:.1f}s')
|
print("build time:", f"{time() - t0:.1f}s")
|
||||||
|
|
||||||
bpy.ops.wm.quit_blender()
|
bpy.ops.wm.quit_blender()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__' :
|
if __name__ == "__main__":
|
||||||
parser = argparse.ArgumentParser(description='build_collection_blends',
|
parser = argparse.ArgumentParser(
|
||||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
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(
|
||||||
parser.add_argument('--category') # Lit la category dans le json et a link tout dans le blend
|
"-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 :
|
if "--" in sys.argv:
|
||||||
index = sys.argv.index('--')
|
index = sys.argv.index("--")
|
||||||
sys.argv = [sys.argv[index-1], *sys.argv[index+1:]]
|
sys.argv = [sys.argv[index - 1], *sys.argv[index + 1 :]]
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
build_collection_blends(**vars(args))
|
build_collection_blends(**vars(args))
|
||||||
+55
-36
@@ -44,10 +44,11 @@ from asset_library.constants import ASSETLIB_FILENAME
|
|||||||
]
|
]
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def create_collection_json(path, source_directory):
|
def create_collection_json(path, source_directory):
|
||||||
'''Create a Json from every marked collection in blends
|
"""Create a Json from every marked collection in blends
|
||||||
contained in folderpath (respect hierachy)
|
contained in folderpath (respect hierachy)
|
||||||
'''
|
"""
|
||||||
|
|
||||||
json_path = Path(path) / ASSETLIB_FILENAME
|
json_path = Path(path) / ASSETLIB_FILENAME
|
||||||
|
|
||||||
@@ -56,75 +57,80 @@ def create_collection_json(path, source_directory):
|
|||||||
# or open all blends and look only for marked collection ? (if versionned, get still get only last)
|
# or open all blends and look only for marked collection ? (if versionned, get still get only last)
|
||||||
|
|
||||||
# get all blend in dir and subdirs (only last when versionned _v???)
|
# get all blend in dir and subdirs (only last when versionned _v???)
|
||||||
blends = get_last_files(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('/') + '/'
|
root_path = Path(source_directory).as_posix().rstrip("/") + "/"
|
||||||
print('root_path: ', root_path)
|
print("root_path: ", root_path)
|
||||||
# open and check data block marked as asset
|
# open and check data block marked as asset
|
||||||
|
|
||||||
category_datas = []
|
category_datas = []
|
||||||
for i, blend in enumerate(blends):
|
for i, blend in enumerate(blends):
|
||||||
fp = Path(blend)
|
fp = Path(blend)
|
||||||
print(f'{i+1}/{len(blends)}')
|
print(f"{i+1}/{len(blends)}")
|
||||||
|
|
||||||
## What is considered a grouping category ? top level folders ? parents[1] ?
|
## What is considered a grouping category ? top level folders ? parents[1] ?
|
||||||
|
|
||||||
## Remove root path and extension
|
## Remove root path and extension
|
||||||
## top level folder ('chars'), problem if blends at root
|
## top level folder ('chars'), problem if blends at root
|
||||||
category = fp.as_posix().replace(root_path, '').split('/')[0]
|
category = fp.as_posix().replace(root_path, "").split("/")[0]
|
||||||
|
|
||||||
## full blend path (chars/perso/perso)
|
## full blend path (chars/perso/perso)
|
||||||
# category = fp.as_posix().replace(root_path, '').rsplit('.', 1)[0]
|
# category = fp.as_posix().replace(root_path, '').rsplit('.', 1)[0]
|
||||||
|
|
||||||
print(category)
|
print(category)
|
||||||
|
|
||||||
with bpy.data.libraries.load(blend, link=True, assets_only=True) as (data_from, data_to):
|
with bpy.data.libraries.load(blend, link=True, assets_only=True) as (
|
||||||
|
data_from,
|
||||||
|
data_to,
|
||||||
|
):
|
||||||
## just listing
|
## just listing
|
||||||
col_name_list = [c for c in data_from.collections]
|
col_name_list = [c for c in data_from.collections]
|
||||||
|
|
||||||
if not col_name_list:
|
if not col_name_list:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
col_list = next((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:
|
if col_list is None:
|
||||||
col_list = []
|
col_list = []
|
||||||
category_data = {
|
category_data = {
|
||||||
'name': category,
|
"name": category,
|
||||||
'id': str(uuid.uuid4()),
|
"id": str(uuid.uuid4()),
|
||||||
'children': col_list,
|
"children": col_list,
|
||||||
}
|
}
|
||||||
category_datas.append(category_data)
|
category_datas.append(category_data)
|
||||||
|
|
||||||
|
|
||||||
blend_source_path = blend.as_posix()
|
blend_source_path = blend.as_posix()
|
||||||
if (project_root := os.environ.get('PROJECT_ROOT')):
|
if project_root := os.environ.get("PROJECT_ROOT"):
|
||||||
blend_source_path = blend_source_path.replace(project_root, '$PROJECT_ROOT')
|
blend_source_path = blend_source_path.replace(project_root, "$PROJECT_ROOT")
|
||||||
|
|
||||||
|
|
||||||
for name in col_name_list:
|
for name in col_name_list:
|
||||||
data = {
|
data = {
|
||||||
'filepath' : blend,
|
"filepath": blend,
|
||||||
'name' : name,
|
"name": name,
|
||||||
# 'tags' : [],
|
# 'tags' : [],
|
||||||
'metadata' : {'filepath': blend_source_path},
|
"metadata": {"filepath": blend_source_path},
|
||||||
}
|
}
|
||||||
|
|
||||||
col_list.append(data)
|
col_list.append(data)
|
||||||
|
|
||||||
json_path.write_text(json.dumps(category_datas, indent='\t'))
|
json_path.write_text(json.dumps(category_datas, indent="\t"))
|
||||||
## create text catalog from json (keep_existing_category ?)
|
## create text catalog from json (keep_existing_category ?)
|
||||||
create_catalog_file(json_path, keep_existing_category=True)
|
create_catalog_file(json_path, keep_existing_category=True)
|
||||||
|
|
||||||
|
|
||||||
def create_collection_library(path, source_directory=None):
|
def create_collection_library(path, source_directory=None):
|
||||||
'''
|
"""
|
||||||
path: store collection library (json and blends database)
|
path: store collection library (json and blends database)
|
||||||
source_directory: if a source is set, rebuild json and library
|
source_directory: if a source is set, rebuild json and library
|
||||||
'''
|
"""
|
||||||
|
|
||||||
if source_directory:
|
if source_directory:
|
||||||
if not Path(source_directory).exists():
|
if not Path(source_directory).exists():
|
||||||
print(f'Source directory not exists: {source_directory}')
|
print(f"Source directory not exists: {source_directory}")
|
||||||
return
|
return
|
||||||
|
|
||||||
## scan source and build json in assetlib dir root
|
## scan source and build json in assetlib dir root
|
||||||
@@ -132,32 +138,45 @@ def create_collection_library(path, source_directory=None):
|
|||||||
|
|
||||||
json_path = Path(path) / ASSETLIB_FILENAME
|
json_path = Path(path) / ASSETLIB_FILENAME
|
||||||
if not json_path.exists():
|
if not json_path.exists():
|
||||||
print(f'No json found at: {json_path}')
|
print(f"No json found at: {json_path}")
|
||||||
return
|
return
|
||||||
|
|
||||||
file_datas = json.loads(json_path.read())
|
file_datas = json.loads(json_path.read())
|
||||||
|
|
||||||
## For each category in json, execute build_assets_blend script
|
## For each category in json, execute build_assets_blend script
|
||||||
script = Path(__file__).parent / 'build_collection_blends.py'
|
script = Path(__file__).parent / "build_collection_blends.py"
|
||||||
#empty_blend = Path(__file__).parent / 'empty_scene.blend'
|
# empty_blend = Path(__file__).parent / 'empty_scene.blend'
|
||||||
|
|
||||||
# for category, asset_datas in file_datas.items():
|
# for category, asset_datas in file_datas.items():
|
||||||
for category_data in file_datas:
|
for category_data in file_datas:
|
||||||
## add an empty blend as second arg
|
## add an empty blend as second arg
|
||||||
cmd = [bpy.app.binary_path, '--python', str(script), '--', '--path', path, '--category', category_data['name']]
|
cmd = [
|
||||||
print('cmd: ', cmd)
|
bpy.app.binary_path,
|
||||||
|
"--python",
|
||||||
|
str(script),
|
||||||
|
"--",
|
||||||
|
"--path",
|
||||||
|
path,
|
||||||
|
"--category",
|
||||||
|
category_data["name"],
|
||||||
|
]
|
||||||
|
print("cmd: ", cmd)
|
||||||
subprocess.call(cmd)
|
subprocess.call(cmd)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__' :
|
if __name__ == "__main__":
|
||||||
parser = argparse.ArgumentParser(description='Create Collection Library',
|
parser = argparse.ArgumentParser(
|
||||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
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 :
|
if "--" in sys.argv:
|
||||||
index = sys.argv.index('--')
|
index = sys.argv.index("--")
|
||||||
sys.argv = [sys.argv[index-1], *sys.argv[index+1:]]
|
sys.argv = [sys.argv[index - 1], *sys.argv[index + 1 :]]
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
create_collection_library(**vars(args))
|
create_collection_library(**vars(args))
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
|
|
||||||
|
|
||||||
@@ -9,6 +8,6 @@ def draw_context_menu(layout):
|
|||||||
|
|
||||||
|
|
||||||
def draw_header(layout):
|
def draw_header(layout):
|
||||||
'''Draw the header of the Asset Browser Window'''
|
"""Draw the header of the Asset Browser Window"""
|
||||||
|
|
||||||
return
|
return
|
||||||
@@ -1,11 +1,10 @@
|
|||||||
|
|
||||||
|
|
||||||
from typing import List, Tuple
|
from typing import List, Tuple
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
|
|
||||||
addon_keymaps: List[Tuple[bpy.types.KeyMap, bpy.types.KeyMapItem]] = []
|
addon_keymaps: List[Tuple[bpy.types.KeyMap, bpy.types.KeyMapItem]] = []
|
||||||
|
|
||||||
|
|
||||||
def register():
|
def register():
|
||||||
wm = bpy.context.window_manager
|
wm = bpy.context.window_manager
|
||||||
addon = wm.keyconfigs.addon
|
addon = wm.keyconfigs.addon
|
||||||
@@ -13,9 +12,12 @@ def register():
|
|||||||
return
|
return
|
||||||
|
|
||||||
km = addon.keymaps.new(name="File Browser Main", space_type="FILE_BROWSER")
|
km = addon.keymaps.new(name="File Browser Main", space_type="FILE_BROWSER")
|
||||||
kmi = km.keymap_items.new("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))
|
addon_keymaps.append((km, kmi))
|
||||||
|
|
||||||
|
|
||||||
def unregister():
|
def unregister():
|
||||||
for km, kmi in addon_keymaps:
|
for km, kmi in addon_keymaps:
|
||||||
km.keymap_items.remove(kmi)
|
km.keymap_items.remove(kmi)
|
||||||
@@ -14,12 +14,11 @@ from asset_library.common.bl_utils import load_col
|
|||||||
from asset_library.common.functions import get_active_library
|
from asset_library.common.functions import get_active_library
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class ASSETLIB_OT_load_asset(Operator):
|
class ASSETLIB_OT_load_asset(Operator):
|
||||||
bl_idname = "assetlib.load_asset"
|
bl_idname = "assetlib.load_asset"
|
||||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||||
bl_label = 'Load Asset'
|
bl_label = "Load Asset"
|
||||||
bl_description = 'Link and override asset in current file'
|
bl_description = "Link and override asset in current file"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context: Context) -> bool:
|
def poll(cls, context: Context) -> bool:
|
||||||
@@ -28,10 +27,10 @@ class ASSETLIB_OT_load_asset(Operator):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
lib = get_active_library()
|
lib = get_active_library()
|
||||||
if not lib or lib.data_type != 'COLLECTION':
|
if not lib or lib.data_type != "COLLECTION":
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if not context.active_file or 'filepath' not in context.active_file.asset_data:
|
if not context.active_file or "filepath" not in context.active_file.asset_data:
|
||||||
cls.poll_message_set("Has not filepath property")
|
cls.poll_message_set("Has not filepath property")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -39,51 +38,49 @@ class ASSETLIB_OT_load_asset(Operator):
|
|||||||
|
|
||||||
def execute(self, context: Context) -> Set[str]:
|
def execute(self, context: Context) -> Set[str]:
|
||||||
|
|
||||||
print('Load Asset')
|
print("Load Asset")
|
||||||
|
|
||||||
lib = get_active_library()
|
lib = get_active_library()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
asset = context.active_file
|
asset = context.active_file
|
||||||
if not asset:
|
if not asset:
|
||||||
self.report({"ERROR"}, 'No asset selected')
|
self.report({"ERROR"}, "No asset selected")
|
||||||
return {'CANCELLED'}
|
return {"CANCELLED"}
|
||||||
|
|
||||||
active_lib = lib.library_type.get_active_asset_library()
|
active_lib = lib.library_type.get_active_asset_library()
|
||||||
asset_path = asset.asset_data['filepath']
|
asset_path = asset.asset_data["filepath"]
|
||||||
asset_path = active_lib.library_type.format_path(asset_path)
|
asset_path = active_lib.library_type.format_path(asset_path)
|
||||||
name = asset.name
|
name = asset.name
|
||||||
|
|
||||||
## set mode to object
|
## set mode to object
|
||||||
if context.mode != 'OBJECT':
|
if context.mode != "OBJECT":
|
||||||
bpy.ops.object.mode_set(mode='OBJECT')
|
bpy.ops.object.mode_set(mode="OBJECT")
|
||||||
|
|
||||||
if not Path(asset_path).exists():
|
if not Path(asset_path).exists():
|
||||||
self.report({'ERROR'}, f'Not exists: {asset_path}')
|
self.report({"ERROR"}, f"Not exists: {asset_path}")
|
||||||
return {'CANCELLED'}
|
return {"CANCELLED"}
|
||||||
|
|
||||||
print('Load collection', asset_path, name)
|
print("Load collection", asset_path, name)
|
||||||
res = load_col(asset_path, name, link=True, override=True, rig_pattern='*_rig')
|
res = load_col(asset_path, name, link=True, override=True, rig_pattern="*_rig")
|
||||||
if res:
|
if res:
|
||||||
if res.type == 'ARMATURE':
|
if res.type == "ARMATURE":
|
||||||
self.report({'INFO'}, f'Override rig {res.name}')
|
self.report({"INFO"}, f"Override rig {res.name}")
|
||||||
elif res.type == 'EMPTY':
|
elif res.type == "EMPTY":
|
||||||
self.report({'INFO'}, f'Instance collection {res.name}')
|
self.report({"INFO"}, f"Instance collection {res.name}")
|
||||||
|
|
||||||
return {'FINISHED'}
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
### --- REGISTER ---
|
### --- REGISTER ---
|
||||||
|
|
||||||
classes = (
|
classes = (ASSETLIB_OT_load_asset,)
|
||||||
ASSETLIB_OT_load_asset,
|
|
||||||
)
|
|
||||||
|
|
||||||
def register():
|
def register():
|
||||||
for cls in classes:
|
for cls in classes:
|
||||||
bpy.utils.register_class(cls)
|
bpy.utils.register_class(cls)
|
||||||
|
|
||||||
|
|
||||||
def unregister():
|
def unregister():
|
||||||
for cls in reversed(classes):
|
for cls in reversed(classes):
|
||||||
bpy.utils.unregister_class(cls)
|
bpy.utils.unregister_class(cls)
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
from asset_library.common import file_utils
|
||||||
|
from asset_library.common import functions
|
||||||
|
from asset_library.common import synchronize
|
||||||
|
from asset_library.common import template
|
||||||
|
from asset_library.common import catalog
|
||||||
|
|
||||||
|
if "bpy" in locals():
|
||||||
|
import importlib
|
||||||
|
|
||||||
|
importlib.reload(file_utils)
|
||||||
|
importlib.reload(functions)
|
||||||
|
importlib.reload(synchronize)
|
||||||
|
importlib.reload(template)
|
||||||
|
importlib.reload(catalog)
|
||||||
|
|
||||||
|
import bpy
|
||||||
@@ -1,35 +1,34 @@
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
Generic Blender functions
|
Generic Blender functions
|
||||||
"""
|
"""
|
||||||
import json
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from fnmatch import fnmatch
|
from fnmatch import fnmatch
|
||||||
from typing import Any, List, Iterable, Optional, Tuple
|
from typing import Any, List, Iterable, Optional, Tuple
|
||||||
|
|
||||||
Datablock = Any
|
Datablock = Any
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
from bpy_extras import asset_utils
|
from bpy_extras import asset_utils
|
||||||
#from asset_library.constants import RESOURCES_DIR
|
from asset_library.constants import RESOURCES_DIR
|
||||||
#from asset_library.common.file_utils import no
|
|
||||||
|
# from asset_library.common.file_utils import no
|
||||||
from os.path import abspath
|
from os.path import abspath
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
from .file_utils import norm_str
|
|
||||||
|
|
||||||
|
class attr_set:
|
||||||
class attr_set():
|
"""Receive a list of tuple [(data_path, "attribute" [, wanted value)] ]
|
||||||
'''Receive a list of tuple [(data_path, "attribute" [, wanted value)] ]
|
|
||||||
entering with-statement : Store existing values, assign wanted value (if any)
|
entering with-statement : Store existing values, assign wanted value (if any)
|
||||||
exiting with-statement: Restore values to their old values
|
exiting with-statement: Restore values to their old values
|
||||||
'''
|
"""
|
||||||
|
|
||||||
def __init__(self, attrib_list):
|
def __init__(self, attrib_list):
|
||||||
self.store = []
|
self.store = []
|
||||||
# item = (prop, attr, [new_val])
|
# item = (prop, attr, [new_val])
|
||||||
for item in attrib_list:
|
for item in attrib_list:
|
||||||
prop, attr = item[:2]
|
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:
|
for item in attrib_list:
|
||||||
prop, attr = item[:2]
|
prop, attr = item[:2]
|
||||||
@@ -38,7 +37,7 @@ class attr_set():
|
|||||||
try:
|
try:
|
||||||
setattr(prop, attr, item[2])
|
setattr(prop, attr, item[2])
|
||||||
except TypeError:
|
except TypeError:
|
||||||
print(f'Cannot set attribute {attr} to {prop}')
|
print(f"Cannot set attribute {attr} to {prop}")
|
||||||
|
|
||||||
def __enter__(self):
|
def __enter__(self):
|
||||||
return self
|
return self
|
||||||
@@ -51,43 +50,37 @@ class attr_set():
|
|||||||
setattr(prop, attr, old_val)
|
setattr(prop, attr, old_val)
|
||||||
|
|
||||||
|
|
||||||
def unique_name(name, names):
|
|
||||||
if name not in names:
|
|
||||||
return name
|
|
||||||
|
|
||||||
i = 1
|
|
||||||
org_name = name
|
|
||||||
while name in names:
|
|
||||||
name = f'{org_name}.{i:03d}'
|
|
||||||
i += 1
|
|
||||||
|
|
||||||
return name
|
|
||||||
|
|
||||||
|
|
||||||
def get_overriden_col(ob, scene=None):
|
def get_overriden_col(ob, scene=None):
|
||||||
scn = scene or bpy.context.scene
|
scn = scene or bpy.context.scene
|
||||||
|
|
||||||
cols = [c for c in bpy.data.collections if scn.user_of_id(c)]
|
cols = [c for c in bpy.data.collections if scn.user_of_id(c)]
|
||||||
|
|
||||||
return next((c for c in cols if ob in c.all_objects[:]
|
return next(
|
||||||
if all(not c.override_library for c in get_col_parents(c))), None)
|
(
|
||||||
|
c
|
||||||
|
for c in cols
|
||||||
|
if ob in c.all_objects[:]
|
||||||
|
if all(not c.override_library for c in get_col_parents(c))
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_view3d_persp():
|
def get_view3d_persp():
|
||||||
windows = bpy.context.window_manager.windows
|
windows = bpy.context.window_manager.windows
|
||||||
view_3ds = [a for w in windows for a in w.screen.areas if a.type == 'VIEW_3D']
|
view_3ds = [a for w in windows for a in w.screen.areas if a.type == "VIEW_3D"]
|
||||||
view_3d = next((a for a in view_3ds if a.spaces.active.region_3d.view_perspective == 'PERSP'), view_3ds[0])
|
view_3d = next(
|
||||||
|
(a for a in view_3ds if a.spaces.active.region_3d.view_perspective == "PERSP"),
|
||||||
|
view_3ds[0],
|
||||||
|
)
|
||||||
return view_3d
|
return view_3d
|
||||||
|
|
||||||
|
|
||||||
def get_viewport():
|
def get_viewport():
|
||||||
screen = bpy.context.screen
|
screen = bpy.context.screen
|
||||||
|
|
||||||
areas = [a for a in screen.areas if a.type == 'VIEW_3D']
|
areas = [a for a in screen.areas if a.type == "VIEW_3D"]
|
||||||
if not areas:
|
areas.sort(key=lambda x: x.width * x.height)
|
||||||
return
|
|
||||||
|
|
||||||
areas.sort(key=lambda x : x.width*x.height)
|
|
||||||
|
|
||||||
return areas[-1]
|
return areas[-1]
|
||||||
|
|
||||||
@@ -102,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]:
|
def area_sorting_key(area: bpy.types.Area) -> Tuple[bool, int]:
|
||||||
"""Return area size in pixels."""
|
"""Return area size in pixels."""
|
||||||
return (area.width * area.height)
|
return area.width * area.height
|
||||||
|
|
||||||
areas = list(suitable_areas(screen))
|
areas = list(suitable_areas(screen))
|
||||||
if not areas:
|
if not areas:
|
||||||
@@ -161,7 +154,9 @@ def active_catalog_id(asset_browser: bpy.types.Area) -> str:
|
|||||||
return params(asset_browser).catalog_id
|
return params(asset_browser).catalog_id
|
||||||
|
|
||||||
|
|
||||||
def get_asset_space_params(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."""
|
"""Return the asset browser parameters given its Area."""
|
||||||
space_data = asset_browser.spaces[0]
|
space_data = asset_browser.spaces[0]
|
||||||
assert asset_utils.SpaceAssetInfo.is_asset_browser(space_data)
|
assert asset_utils.SpaceAssetInfo.is_asset_browser(space_data)
|
||||||
@@ -170,7 +165,7 @@ def get_asset_space_params(asset_browser: bpy.types.Area) -> bpy.types.FileAsset
|
|||||||
|
|
||||||
def refresh_asset_browsers():
|
def refresh_asset_browsers():
|
||||||
for area in suitable_areas(bpy.context.screen):
|
for area in suitable_areas(bpy.context.screen):
|
||||||
bpy.ops.asset.library_refresh({"area": area, 'region': area.regions[3]})
|
bpy.ops.asset.library_refresh({"area": area, "region": area.regions[3]})
|
||||||
|
|
||||||
|
|
||||||
def tag_redraw(screen: bpy.types.Screen) -> None:
|
def tag_redraw(screen: bpy.types.Screen) -> None:
|
||||||
@@ -179,6 +174,7 @@ def tag_redraw(screen: bpy.types.Screen) -> None:
|
|||||||
for area in suitable_areas(screen):
|
for area in suitable_areas(screen):
|
||||||
area.tag_redraw()
|
area.tag_redraw()
|
||||||
|
|
||||||
|
|
||||||
# def get_blender_command(file=None, script=None, background=True, **args):
|
# def get_blender_command(file=None, script=None, background=True, **args):
|
||||||
# '''Return a Blender Command as a list to be used in a subprocess'''
|
# '''Return a Blender Command as a list to be used in a subprocess'''
|
||||||
|
|
||||||
@@ -197,6 +193,7 @@ def tag_redraw(screen: bpy.types.Screen) -> None:
|
|||||||
|
|
||||||
# return cmd
|
# return cmd
|
||||||
|
|
||||||
|
|
||||||
def norm_value(value):
|
def norm_value(value):
|
||||||
if isinstance(value, (tuple, list)):
|
if isinstance(value, (tuple, list)):
|
||||||
values = []
|
values = []
|
||||||
@@ -215,35 +212,34 @@ def norm_value(value):
|
|||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
def norm_arg(arg_name, format=str.lower, prefix='--', separator='-'):
|
def norm_arg(arg_name, format=str.lower, prefix="--", separator="-"):
|
||||||
arg_name = norm_str(arg_name, format=format, separator=separator)
|
arg_name = norm_str(arg_name, format=format, separator=separator)
|
||||||
|
|
||||||
return prefix + arg_name
|
return prefix + arg_name
|
||||||
|
|
||||||
|
|
||||||
def get_bl_cmd(blender=None, background=False, factory_startup=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]
|
cmd = [str(blender)] if blender else [bpy.app.binary_path]
|
||||||
|
|
||||||
if background:
|
if background:
|
||||||
cmd += ['--background']
|
cmd += ["--background"]
|
||||||
|
|
||||||
if not focus and not background:
|
if not focus and not background:
|
||||||
cmd += ['--no-window-focus']
|
cmd += ["--no-window-focus"]
|
||||||
cmd += ['--window-geometry', '5000', '0', '10', '10']
|
cmd += ["--window-geometry", "5000", "0", "10", "10"]
|
||||||
|
|
||||||
cmd += ['--python-use-system-env']
|
cmd += ["--python-use-system-env"]
|
||||||
|
|
||||||
if factory_startup:
|
|
||||||
cmd += ['--factory-startup']
|
|
||||||
|
|
||||||
if blendfile:
|
if blendfile:
|
||||||
cmd += [str(blendfile)]
|
cmd += [str(blendfile)]
|
||||||
|
|
||||||
if script:
|
if script:
|
||||||
cmd += ['--python', str(script)]
|
cmd += ["--python", str(script)]
|
||||||
|
|
||||||
if kargs:
|
if kargs:
|
||||||
cmd += ['--']
|
cmd += ["--"]
|
||||||
for k, v in kargs.items():
|
for k, v in kargs.items():
|
||||||
k = norm_arg(k)
|
k = norm_arg(k)
|
||||||
v = norm_value(v)
|
v = norm_value(v)
|
||||||
@@ -258,15 +254,35 @@ def get_bl_cmd(blender=None, background=False, factory_startup=False, focus=True
|
|||||||
|
|
||||||
|
|
||||||
def get_addon_prefs():
|
def get_addon_prefs():
|
||||||
addon_name = __package__.split('.')[0]
|
addon_name = __package__.split(".")[0]
|
||||||
return bpy.context.preferences.addons[addon_name].preferences
|
return bpy.context.preferences.addons[addon_name].preferences
|
||||||
|
|
||||||
|
|
||||||
|
def thumbnail_blend_file(input_blend, output_img):
|
||||||
|
input_blend = Path(input_blend).resolve()
|
||||||
|
output_img = Path(output_img).resolve()
|
||||||
|
|
||||||
|
print(f"Thumbnailing {input_blend} to {output_img}")
|
||||||
|
blender_thumbnailer = Path(bpy.app.binary_path).parent / "blender-thumbnailer"
|
||||||
|
|
||||||
|
output_img.parent.mkdir(exist_ok=True, parents=True)
|
||||||
|
|
||||||
|
subprocess.call([blender_thumbnailer, str(input_blend), str(output_img)])
|
||||||
|
|
||||||
|
success = output_img.exists()
|
||||||
|
|
||||||
|
if not success:
|
||||||
|
empty_preview = RESOURCES_DIR / "empty_preview.png"
|
||||||
|
shutil.copy(str(empty_preview), str(output_img))
|
||||||
|
|
||||||
|
return success
|
||||||
|
|
||||||
|
|
||||||
def get_col_parents(col, root=None, cols=None):
|
def get_col_parents(col, root=None, cols=None):
|
||||||
'''Return a list of parents collections of passed col
|
"""Return a list of parents collections of passed col
|
||||||
root : Pass a collection to search in (recursive)
|
root : Pass a collection to search in (recursive)
|
||||||
else search in master collection
|
else search in master collection
|
||||||
'''
|
"""
|
||||||
if cols is None:
|
if cols is None:
|
||||||
cols = []
|
cols = []
|
||||||
|
|
||||||
@@ -283,13 +299,20 @@ def get_col_parents(col, root=None, cols=None):
|
|||||||
|
|
||||||
|
|
||||||
def get_overriden_col(ob, scene=None):
|
def get_overriden_col(ob, scene=None):
|
||||||
'''Get the collection use for making the override'''
|
"""Get the collection use for making the override"""
|
||||||
scn = scene or bpy.context.scene
|
scn = scene or bpy.context.scene
|
||||||
|
|
||||||
cols = [c for c in bpy.data.collections if scn.user_of_id(c)]
|
cols = [c for c in bpy.data.collections if scn.user_of_id(c)]
|
||||||
|
|
||||||
return next((c for c in cols if ob in c.all_objects[:]
|
return next(
|
||||||
if all(not c.override_library for c in get_col_parents(c))), None)
|
(
|
||||||
|
c
|
||||||
|
for c in cols
|
||||||
|
if ob in c.all_objects[:]
|
||||||
|
if all(not c.override_library for c in get_col_parents(c))
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def load_assets_from(filepath: Path) -> List[Datablock]:
|
def load_assets_from(filepath: Path) -> List[Datablock]:
|
||||||
@@ -336,32 +359,28 @@ def has_assets(filepath: Path) -> bool:
|
|||||||
|
|
||||||
|
|
||||||
def copy_frames(start, end, offset, path):
|
def copy_frames(start, end, offset, path):
|
||||||
for i in range (start, end):
|
for i in range(start, end):
|
||||||
src = path.replace('####', f'{i:04d}')
|
src = path.replace("####", f"{i:04d}")
|
||||||
dst = src.replace(src.split('_')[-1].split('.')[0], f'{i+offset:04d}')
|
dst = src.replace(src.split("_")[-1].split(".")[0], f"{i+offset:04d}")
|
||||||
shutil.copy2(src, dst)
|
shutil.copy2(src, dst)
|
||||||
|
|
||||||
|
|
||||||
def split_path(path) :
|
def split_path(path):
|
||||||
try :
|
try:
|
||||||
bone_name = path.split('["')[1].split('"]')[0]
|
bone_name = path.split('["')[1].split('"]')[0]
|
||||||
except :
|
except:
|
||||||
bone_name = None
|
bone_name = None
|
||||||
try :
|
try:
|
||||||
prop_name = path.split('["')[2].split('"]')[0]
|
prop_name = path.split('["')[2].split('"]')[0]
|
||||||
except :
|
except:
|
||||||
prop_name = path.split('.')[-1]
|
prop_name = path.split(".")[-1]
|
||||||
|
|
||||||
return bone_name, prop_name
|
return bone_name, prop_name
|
||||||
|
|
||||||
|
|
||||||
def get_asset_type(asset_type):
|
def load_datablocks(
|
||||||
data_types = { p.fixed_type.identifier: p.identifier for p in
|
src, names=None, type="objects", link=True, expr=None, assets_only=False
|
||||||
bpy.types.BlendData.bl_rna.properties if hasattr(p, 'fixed_type')}
|
) -> list:
|
||||||
return data_types[asset_type]
|
|
||||||
|
|
||||||
|
|
||||||
def load_datablocks(src, names=None, type='objects', link=True, expr=None, assets_only=False) -> list:
|
|
||||||
return_list = not isinstance(names, str)
|
return_list = not isinstance(names, str)
|
||||||
names = names or []
|
names = names or []
|
||||||
|
|
||||||
@@ -370,9 +389,12 @@ def load_datablocks(src, names=None, type='objects', link=True, expr=None, asset
|
|||||||
|
|
||||||
if isinstance(expr, str):
|
if isinstance(expr, str):
|
||||||
pattern = expr
|
pattern = expr
|
||||||
expr = lambda x : fnmatch(x, pattern)
|
expr = lambda x: fnmatch(x, pattern)
|
||||||
|
|
||||||
with bpy.data.libraries.load(str(src), link=link,assets_only=assets_only) as (data_from, data_to):
|
with bpy.data.libraries.load(str(src), link=link, assets_only=assets_only) as (
|
||||||
|
data_from,
|
||||||
|
data_to,
|
||||||
|
):
|
||||||
datablocks = getattr(data_from, type)
|
datablocks = getattr(data_from, type)
|
||||||
if expr:
|
if expr:
|
||||||
names += [i for i in datablocks if expr(i)]
|
names += [i for i in datablocks if expr(i)]
|
||||||
@@ -389,23 +411,26 @@ def load_datablocks(src, names=None, type='objects', link=True, expr=None, asset
|
|||||||
elif datablocks:
|
elif datablocks:
|
||||||
return datablocks[0]
|
return datablocks[0]
|
||||||
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
# --- Collection handling
|
# --- Collection handling
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def col_as_asset(col, verbose=False):
|
def col_as_asset(col, verbose=False):
|
||||||
if col is None:
|
if col is None:
|
||||||
return
|
return
|
||||||
if verbose:
|
if verbose:
|
||||||
print('linking:', col.name)
|
print("linking:", col.name)
|
||||||
pcol = bpy.data.collections.new(col.name)
|
pcol = bpy.data.collections.new(col.name)
|
||||||
bpy.context.scene.collection.children.link(pcol)
|
bpy.context.scene.collection.children.link(pcol)
|
||||||
pcol.children.link(col)
|
pcol.children.link(col)
|
||||||
pcol.asset_mark()
|
pcol.asset_mark()
|
||||||
return pcol
|
return pcol
|
||||||
|
|
||||||
|
|
||||||
def load_col(filepath, name, link=True, override=True, rig_pattern=None, context=None):
|
def load_col(filepath, name, link=True, override=True, rig_pattern=None, context=None):
|
||||||
'''Link a collection by name from a file and override if has armature'''
|
"""Link a collection by name from a file and override if has armature"""
|
||||||
|
|
||||||
# with bpy.data.libraries.load(filepath, link=link) as (data_from, data_to):
|
# with bpy.data.libraries.load(filepath, link=link) as (data_from, data_to):
|
||||||
# data_to.collections = [c for c in data_from.collections if c == name]
|
# data_to.collections = [c for c in data_from.collections if c == name]
|
||||||
@@ -414,12 +439,12 @@ def load_col(filepath, name, link=True, override=True, rig_pattern=None, context
|
|||||||
# return data_to.collections[0]
|
# return data_to.collections[0]
|
||||||
context = context or bpy.context
|
context = context or bpy.context
|
||||||
|
|
||||||
col = load_datablocks(filepath, name, link=link, type='collections')
|
col = load_datablocks(filepath, name, link=link, type="collections")
|
||||||
|
|
||||||
## create instance object
|
## create instance object
|
||||||
inst = bpy.data.objects.new(col.name, None)
|
inst = bpy.data.objects.new(col.name, None)
|
||||||
inst.instance_collection = col
|
inst.instance_collection = col
|
||||||
inst.instance_type = 'COLLECTION'
|
inst.instance_type = "COLLECTION"
|
||||||
context.scene.collection.objects.link(inst)
|
context.scene.collection.objects.link(inst)
|
||||||
|
|
||||||
# make active
|
# make active
|
||||||
@@ -429,7 +454,7 @@ def load_col(filepath, name, link=True, override=True, rig_pattern=None, context
|
|||||||
## simple object (no armatures)
|
## simple object (no armatures)
|
||||||
if not link or not override:
|
if not link or not override:
|
||||||
return inst
|
return inst
|
||||||
if not next((o for o in col.all_objects if o.type == 'ARMATURE'), None):
|
if not next((o for o in col.all_objects if o.type == "ARMATURE"), None):
|
||||||
return inst
|
return inst
|
||||||
|
|
||||||
## Create the override
|
## Create the override
|
||||||
@@ -437,18 +462,21 @@ def load_col(filepath, name, link=True, override=True, rig_pattern=None, context
|
|||||||
parent_cols = inst.users_collection
|
parent_cols = inst.users_collection
|
||||||
child_cols = [child for pcol in parent_cols for child in pcol.children]
|
child_cols = [child for pcol in parent_cols for child in pcol.children]
|
||||||
|
|
||||||
params = {'active_object': inst, 'selected_objects': [inst]}
|
params = {"active_object": inst, "selected_objects": [inst]}
|
||||||
try:
|
try:
|
||||||
bpy.ops.object.make_override_library(params)
|
bpy.ops.object.make_override_library(params)
|
||||||
|
|
||||||
## check which collection is new in parents collection
|
## check which collection is new in parents collection
|
||||||
asset_col = next((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:
|
if not asset_col:
|
||||||
print('Overriden, but no collection found !!')
|
print("Overriden, but no collection found !!")
|
||||||
return
|
return
|
||||||
|
|
||||||
for ob in asset_col.all_objects:
|
for ob in asset_col.all_objects:
|
||||||
if ob.type != 'ARMATURE':
|
if ob.type != "ARMATURE":
|
||||||
continue
|
continue
|
||||||
if rig_pattern and not fnmatch(ob.name, rig_pattern):
|
if rig_pattern and not fnmatch(ob.name, rig_pattern):
|
||||||
continue
|
continue
|
||||||
@@ -460,16 +488,19 @@ def load_col(filepath, name, link=True, override=True, rig_pattern=None, context
|
|||||||
return ob
|
return ob
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f'Override failed on {col.name}')
|
print(f"Override failed on {col.name}")
|
||||||
print(e)
|
print(e)
|
||||||
|
|
||||||
return inst
|
return inst
|
||||||
|
|
||||||
|
|
||||||
def get_preview(asset_path='', asset_name=''):
|
def get_preview(asset_path="", asset_name=""):
|
||||||
asset_preview_dir = Path(asset_path).parents[1]
|
asset_preview_dir = Path(asset_path).parents[1]
|
||||||
name = asset_name.lower()
|
name = asset_name.lower()
|
||||||
return next((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):
|
def get_object_libraries(ob):
|
||||||
if ob is None:
|
if ob is None:
|
||||||
@@ -479,7 +510,7 @@ def get_object_libraries(ob):
|
|||||||
if ob.data:
|
if ob.data:
|
||||||
libraries += [ob.data.library]
|
libraries += [ob.data.library]
|
||||||
|
|
||||||
if ob.type in ('MESH', 'CURVE'):
|
if ob.type in ("MESH", "CURVE"):
|
||||||
libraries += [m.library for m in ob.data.materials if m]
|
libraries += [m.library for m in ob.data.materials if m]
|
||||||
|
|
||||||
filepaths = []
|
filepaths = []
|
||||||
@@ -494,85 +525,3 @@ def get_object_libraries(ob):
|
|||||||
filepaths.append(absolute_filepath)
|
filepaths.append(absolute_filepath)
|
||||||
|
|
||||||
return filepaths
|
return filepaths
|
||||||
|
|
||||||
|
|
||||||
def clean_name(name):
|
|
||||||
if re.match(r'(.*)\.\d{3}$', name):
|
|
||||||
return name[:-4]
|
|
||||||
return name
|
|
||||||
|
|
||||||
|
|
||||||
def is_node_groups_duplicate(node_groups):
|
|
||||||
node_group_types = sorted([n.type for n in node_groups[0].nodes])
|
|
||||||
return all( sorted([n.type for n in ng.nodes]) ==
|
|
||||||
node_group_types for ng in node_groups[1:])
|
|
||||||
|
|
||||||
|
|
||||||
def is_images_duplicate(images):
|
|
||||||
return all( img.filepath == images[0].filepath for image in images)
|
|
||||||
|
|
||||||
|
|
||||||
def is_materials_duplicate(materials):
|
|
||||||
node_group_types = sorted([n.type for n in materials[0].node_tree.nodes])
|
|
||||||
return all( sorted([n.type for n in mat.node_tree.nodes]) ==
|
|
||||||
node_group_types for mat in materials[1:])
|
|
||||||
|
|
||||||
|
|
||||||
def merge_datablock_duplicates(datablocks, blend_data, force=False):
|
|
||||||
"""Merging materials, node_groups or images based on name .001, .002"""
|
|
||||||
|
|
||||||
failed = []
|
|
||||||
merged = []
|
|
||||||
|
|
||||||
datablocks = list(datablocks)
|
|
||||||
|
|
||||||
#blend_data = get_asset_type(datablocks[0].bl_rna.identifier)
|
|
||||||
if blend_data == 'materials':
|
|
||||||
is_datablock_duplicate = is_materials_duplicate
|
|
||||||
elif blend_data == 'node_groups':
|
|
||||||
is_datablock_duplicate = is_node_groups_duplicate
|
|
||||||
elif blend_data == 'images':
|
|
||||||
is_datablock_duplicate = is_images_duplicate
|
|
||||||
else:
|
|
||||||
raise Exception(f'Type, {blend_data} not supported')
|
|
||||||
|
|
||||||
# Group by name
|
|
||||||
groups = {}
|
|
||||||
for datablock in images:
|
|
||||||
groups.setdefault(clean_name(datablock.name), []).append(datablock)
|
|
||||||
|
|
||||||
for datablock in blend_data:
|
|
||||||
name = clean_name(datablock.name)
|
|
||||||
if name in groups and datablock not in groups[name]:
|
|
||||||
groups[name].append(datablock)
|
|
||||||
|
|
||||||
print("\nMerge Duplicate Datablocks...")
|
|
||||||
|
|
||||||
for group in groups.values():
|
|
||||||
if len(group) == 1:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if not force:
|
|
||||||
datablocks.sort(key=lambda x : x.name, reverse=True)
|
|
||||||
|
|
||||||
for datablock in datablocks[1:]:
|
|
||||||
is_duplicate = is_datablock_duplicate((datablock, datablocks[0]))
|
|
||||||
|
|
||||||
if not is_duplicate and not force:
|
|
||||||
failed.append((datablock.name, datablocks[0].name))
|
|
||||||
print(f'Cannot merge Datablock {datablocks.name} with {datablocks[0].name} they are different')
|
|
||||||
continue
|
|
||||||
|
|
||||||
merged.append((datablock.name, datablocks[0].name))
|
|
||||||
print(f'Merge Datablock {datablock.name} into {datablocks[0].name}')
|
|
||||||
|
|
||||||
datablock.user_remap(datablocks[0])
|
|
||||||
datablocks.remove(datablock)
|
|
||||||
blend_data.remove(datablock)
|
|
||||||
|
|
||||||
# Rename groups if it has no duplicate left
|
|
||||||
for datablocks in groups.values():
|
|
||||||
if len(datablocks) == 1 and not datablocks[0].library:
|
|
||||||
datablocks[0].name = clean_name(datablocks[0].name)
|
|
||||||
|
|
||||||
return merged, failed
|
|
||||||
@@ -1,19 +1,11 @@
|
|||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import uuid
|
import uuid
|
||||||
import bpy
|
import bpy
|
||||||
from .file_utils import cache
|
|
||||||
|
|
||||||
@cache(1)
|
|
||||||
def read_catalog(library_path):
|
|
||||||
catalog = Catalog(library_path)
|
|
||||||
catalog.read()
|
|
||||||
|
|
||||||
return catalog
|
|
||||||
|
|
||||||
|
|
||||||
class CatalogItem:
|
class CatalogItem:
|
||||||
"""Represent a single item of a catalog"""
|
"""Represent a single item of a catalog"""
|
||||||
|
|
||||||
def __init__(self, catalog, path=None, name=None, id=None):
|
def __init__(self, catalog, path=None, name=None, id=None):
|
||||||
|
|
||||||
self.catalog = catalog
|
self.catalog = catalog
|
||||||
@@ -33,10 +25,10 @@ class CatalogItem:
|
|||||||
|
|
||||||
def norm_name(self, name):
|
def norm_name(self, name):
|
||||||
"""Get a norm name from a catalog_path entry"""
|
"""Get a norm name from a catalog_path entry"""
|
||||||
return name.replace('/', '-')
|
return name.replace("/", "-")
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f'CatalogItem(name={self.name}, path={self.path}, id={self.id})'
|
return f"CatalogItem(name={self.name}, path={self.path}, id={self.id})"
|
||||||
|
|
||||||
|
|
||||||
class CatalogContext:
|
class CatalogContext:
|
||||||
@@ -68,11 +60,12 @@ class CatalogContext:
|
|||||||
if self.active_item:
|
if self.active_item:
|
||||||
return self.active_item.path
|
return self.active_item.path
|
||||||
|
|
||||||
return ''
|
return ""
|
||||||
|
|
||||||
|
|
||||||
class Catalog:
|
class Catalog:
|
||||||
"""Represent the catalog of the blender asset browser library"""
|
"""Represent the catalog of the blender asset browser library"""
|
||||||
|
|
||||||
def __init__(self, directory=None):
|
def __init__(self, directory=None):
|
||||||
|
|
||||||
self.directory = None
|
self.directory = None
|
||||||
@@ -87,7 +80,7 @@ class Catalog:
|
|||||||
def filepath(self):
|
def filepath(self):
|
||||||
"""Get the filepath of the catalog text file relative to the directory"""
|
"""Get the filepath of the catalog text file relative to the directory"""
|
||||||
if self.directory:
|
if self.directory:
|
||||||
return self.directory /'blender_assets.cats.txt'
|
return self.directory / "blender_assets.cats.txt"
|
||||||
|
|
||||||
def read(self):
|
def read(self):
|
||||||
"""Read the catalog file of the library target directory or of the specified directory"""
|
"""Read the catalog file of the library target directory or of the specified directory"""
|
||||||
@@ -97,13 +90,15 @@ class Catalog:
|
|||||||
|
|
||||||
self._data.clear()
|
self._data.clear()
|
||||||
|
|
||||||
print(f'Read catalog from {self.filepath}')
|
print(f"Read catalog from {self.filepath}")
|
||||||
for line in self.filepath.read_text(encoding="utf-8").split('\n'):
|
for line in self.filepath.read_text(encoding="utf-8").split("\n"):
|
||||||
if line.startswith(('VERSION', '#')) or not line:
|
if line.startswith(("VERSION", "#")) or not line:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
cat_id, cat_path, cat_name = line.split(':')
|
cat_id, cat_path, cat_name = line.split(":")
|
||||||
self._data[cat_id] = CatalogItem(self, name=cat_name, id=cat_id, path=cat_path)
|
self._data[cat_id] = CatalogItem(
|
||||||
|
self, name=cat_name, id=cat_id, path=cat_path
|
||||||
|
)
|
||||||
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
@@ -111,19 +106,19 @@ class Catalog:
|
|||||||
"""Write the catalog file in the library target directory or of the specified directory"""
|
"""Write the catalog file in the library target directory or of the specified directory"""
|
||||||
|
|
||||||
if not self.filepath:
|
if not self.filepath:
|
||||||
raise Exception(f'Cannot write catalog {self} no filepath setted')
|
raise Exception(f"Cannot write catalog {self} no filepath setted")
|
||||||
|
|
||||||
lines = ['VERSION 1', '']
|
lines = ["VERSION 1", ""]
|
||||||
|
|
||||||
catalog_items = list(self)
|
catalog_items = list(self)
|
||||||
if sort:
|
if sort:
|
||||||
catalog_items.sort(key=lambda x : x.path)
|
catalog_items.sort(key=lambda x: x.path)
|
||||||
|
|
||||||
for catalog_item in catalog_items:
|
for catalog_item in catalog_items:
|
||||||
lines.append(f"{catalog_item.id}:{catalog_item.path}:{catalog_item.name}")
|
lines.append(f"{catalog_item.id}:{catalog_item.path}:{catalog_item.name}")
|
||||||
|
|
||||||
print(f'Write Catalog at: {self.filepath}')
|
print(f"Write Catalog at: {self.filepath}")
|
||||||
self.filepath.write_text('\n'.join(lines), encoding="utf-8")
|
self.filepath.write_text("\n".join(lines), encoding="utf-8")
|
||||||
|
|
||||||
def get(self, path=None, id=None, fallback=None):
|
def get(self, path=None, id=None, fallback=None):
|
||||||
"""Found a catalog item by is path or id"""
|
"""Found a catalog item by is path or id"""
|
||||||
@@ -148,7 +143,7 @@ class Catalog:
|
|||||||
if catalog_item:
|
if catalog_item:
|
||||||
return self._data.pop(catalog_item.id)
|
return self._data.pop(catalog_item.id)
|
||||||
|
|
||||||
print(f'Warning: {catalog_item} cannot be remove, not in {self}')
|
print(f"Warning: {catalog_item} cannot be remove, not in {self}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def add(self, catalog_path):
|
def add(self, catalog_path):
|
||||||
@@ -171,17 +166,21 @@ class Catalog:
|
|||||||
return cat_item
|
return cat_item
|
||||||
|
|
||||||
def update(self, catalogs):
|
def update(self, catalogs):
|
||||||
'Add or remove catalog entries if on the list given or not'
|
"Add or remove catalog entries if on the list given or not"
|
||||||
|
|
||||||
catalogs = set(catalogs) # Remove doubles
|
catalogs = set(catalogs) # Remove doubles
|
||||||
|
|
||||||
added = [c for c in catalogs if not self.get(path=c)]
|
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]
|
removed = [c.path for c in self if c.path not in catalogs]
|
||||||
|
|
||||||
if added:
|
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:
|
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:
|
for catalog_item in removed:
|
||||||
self.remove(catalog_item)
|
self.remove(catalog_item)
|
||||||
@@ -199,10 +198,10 @@ class Catalog:
|
|||||||
return self._data[key]
|
return self._data[key]
|
||||||
|
|
||||||
def __contains__(self, item):
|
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
|
return item in self._data
|
||||||
else:
|
else:
|
||||||
return item in self
|
return item in self
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f'Catalog(filepath={self.filepath})'
|
return f"Catalog(filepath={self.filepath})"
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
"""Generic python functions to make operation on file and names"""
|
"""Generic python functions to make operation on file and names"""
|
||||||
|
|
||||||
import fnmatch
|
import fnmatch
|
||||||
@@ -12,8 +11,6 @@ from pathlib import Path
|
|||||||
import importlib
|
import importlib
|
||||||
import sys
|
import sys
|
||||||
import shutil
|
import shutil
|
||||||
from functools import wraps
|
|
||||||
from time import perf_counter
|
|
||||||
|
|
||||||
import contextlib
|
import contextlib
|
||||||
|
|
||||||
@@ -30,14 +27,16 @@ def cd(path):
|
|||||||
|
|
||||||
|
|
||||||
def install_module(module_name, package_name=None):
|
def install_module(module_name, package_name=None):
|
||||||
'''Install a python module with pip or return it if already installed'''
|
"""Install a python module with pip or return it if already installed"""
|
||||||
try:
|
try:
|
||||||
module = importlib.import_module(module_name)
|
module = importlib.import_module(module_name)
|
||||||
except ModuleNotFoundError:
|
except ModuleNotFoundError:
|
||||||
print(f'Installing Module {module_name} ....')
|
print(f"Installing Module {module_name} ....")
|
||||||
|
|
||||||
subprocess.call([sys.executable, '-m', 'ensurepip'])
|
subprocess.call([sys.executable, "-m", "ensurepip"])
|
||||||
subprocess.call([sys.executable, '-m', 'pip', 'install', package_name or module_name])
|
subprocess.call(
|
||||||
|
[sys.executable, "-m", "pip", "install", package_name or module_name]
|
||||||
|
)
|
||||||
|
|
||||||
module = importlib.import_module(module_name)
|
module = importlib.import_module(module_name)
|
||||||
|
|
||||||
@@ -56,49 +55,59 @@ def import_module_from_path(path):
|
|||||||
|
|
||||||
return mod
|
return mod
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f'Cannot import file {path}')
|
print(f"Cannot import file {path}")
|
||||||
print(e)
|
print(e)
|
||||||
|
|
||||||
|
|
||||||
def norm_str(string, separator='_', format=str.lower, padding=0):
|
def norm_str(string, separator="_", format=str.lower, padding=0):
|
||||||
string = str(string)
|
string = str(string)
|
||||||
string = string.replace('_', ' ')
|
string = string.replace("_", " ")
|
||||||
string = string.replace('-', ' ')
|
string = string.replace("-", " ")
|
||||||
string = re.sub('[ ]+', ' ', string)
|
string = re.sub("[ ]+", " ", string)
|
||||||
string = re.sub('[ ]+\/[ ]+', '/', string)
|
string = re.sub("[ ]+\/[ ]+", "/", string)
|
||||||
string = string.strip()
|
string = string.strip()
|
||||||
|
|
||||||
if format:
|
if format:
|
||||||
string = format(string)
|
string = format(string)
|
||||||
|
|
||||||
# Padd rightest number
|
# Padd rightest number
|
||||||
string = re.sub(r'(\d+)(?!.*\d)', lambda x : x.group(1).zfill(padding), string)
|
string = re.sub(r"(\d+)(?!.*\d)", lambda x: x.group(1).zfill(padding), string)
|
||||||
|
|
||||||
string = string.replace(' ', separator)
|
string = string.replace(" ", separator)
|
||||||
string = unicodedata.normalize('NFKD', string).encode('ASCII', 'ignore').decode("utf-8")
|
string = (
|
||||||
|
unicodedata.normalize("NFKD", string).encode("ASCII", "ignore").decode("utf-8")
|
||||||
|
)
|
||||||
|
|
||||||
return string
|
return string
|
||||||
|
|
||||||
|
|
||||||
def remove_version(filepath):
|
def remove_version(filepath):
|
||||||
pattern = '_v[0-9]+\.'
|
pattern = "_v[0-9]+\."
|
||||||
search = re.search(pattern, filepath)
|
search = re.search(pattern, filepath)
|
||||||
|
|
||||||
if search:
|
if search:
|
||||||
filepath = filepath.replace(search.group()[:-1], '')
|
filepath = filepath.replace(search.group()[:-1], "")
|
||||||
|
|
||||||
return Path(filepath).name
|
return Path(filepath).name
|
||||||
|
|
||||||
|
|
||||||
def is_exclude(name, patterns) -> bool:
|
def is_exclude(name, patterns) -> bool:
|
||||||
# from fnmatch import fnmatch
|
# from fnmatch import fnmatch
|
||||||
if not isinstance(patterns, (list,tuple)) :
|
if not isinstance(patterns, (list, tuple)):
|
||||||
patterns = [patterns]
|
patterns = [patterns]
|
||||||
return any([fnmatch(name, p) for p in 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:
|
def get_last_files(
|
||||||
'''Recursively get last(s) file(s) (when there is multiple versions) in passed directory
|
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.
|
root -> str: Filepath of the folder to scan.
|
||||||
pattern -> str: Regex pattern to group files.
|
pattern -> str: Regex pattern to group files.
|
||||||
only_matching -> bool: Discard files that aren't matched by regex pattern.
|
only_matching -> bool: Discard files that aren't matched by regex pattern.
|
||||||
@@ -106,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.
|
ex_dir -> list : List of fn_match pattern of directory name to skip.
|
||||||
keep -> int: Number of lasts versions to keep when there are mutliple versionned files (e.g: 1 keep only last).
|
keep -> int: Number of lasts versions to keep when there are mutliple versionned files (e.g: 1 keep only last).
|
||||||
verbose -> bool: Print infos in console.
|
verbose -> bool: Print infos in console.
|
||||||
'''
|
"""
|
||||||
|
|
||||||
files = []
|
files = []
|
||||||
if ex_file is None:
|
if ex_file is None:
|
||||||
@@ -120,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()]
|
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 not re.search(pattern, allfiles[i].name):
|
||||||
if only_matching:
|
if only_matching:
|
||||||
allfiles.pop(i)
|
allfiles.pop(i)
|
||||||
@@ -128,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)
|
files.append(allfiles.pop(i).path)
|
||||||
|
|
||||||
# separate remaining files in prefix grouped lists
|
# 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
|
# get only item last of each sorted grouplist
|
||||||
for l in lilist:
|
for l in lilist:
|
||||||
@@ -137,14 +151,22 @@ def get_last_files(root, pattern=r'_v\d{3}\.\w+', only_matching=False, ex_file=N
|
|||||||
files.append(f.path)
|
files.append(f.path)
|
||||||
|
|
||||||
if verbose and len(l) > 1:
|
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):
|
if ex_dir and is_exclude(d.name, ex_dir):
|
||||||
# skip folder with excluded name
|
# skip folder with excluded name
|
||||||
continue
|
continue
|
||||||
files += get_last_files(
|
files += get_last_files(
|
||||||
d.path, 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)
|
return sorted(files)
|
||||||
|
|
||||||
@@ -157,20 +179,20 @@ def copy_file(src, dst, only_new=False, only_recent=False):
|
|||||||
return
|
return
|
||||||
|
|
||||||
dst.parent.mkdir(exist_ok=True, parents=True)
|
dst.parent.mkdir(exist_ok=True, parents=True)
|
||||||
print(f'Copy file from {src} to {dst}')
|
print(f"Copy file from {src} to {dst}")
|
||||||
if platform.system() == 'Windows':
|
if platform.system() == "Windows":
|
||||||
subprocess.call(['copy', str(src), str(dst)], shell=True)
|
subprocess.call(["copy", str(src), str(dst)], shell=True)
|
||||||
else:
|
else:
|
||||||
subprocess.call(['cp', str(src), str(dst)])
|
subprocess.call(["cp", str(src), str(dst)])
|
||||||
|
|
||||||
|
|
||||||
def copy_dir(src, dst, only_new=False, only_recent=False, excludes=['.*'], includes=[]):
|
def copy_dir(src, dst, only_new=False, only_recent=False, excludes=[".*"], includes=[]):
|
||||||
src, dst = Path(src), Path(dst)
|
src, dst = Path(src), Path(dst)
|
||||||
|
|
||||||
if includes:
|
if includes:
|
||||||
includes = r'|'.join([fnmatch.translate(x) for x in includes])
|
includes = r"|".join([fnmatch.translate(x) for x in includes])
|
||||||
if excludes:
|
if excludes:
|
||||||
excludes = r'|'.join([fnmatch.translate(x) for x in excludes])
|
excludes = r"|".join([fnmatch.translate(x) for x in excludes])
|
||||||
|
|
||||||
if dst.is_dir():
|
if dst.is_dir():
|
||||||
dst.mkdir(exist_ok=True, parents=True)
|
dst.mkdir(exist_ok=True, parents=True)
|
||||||
@@ -181,35 +203,37 @@ def copy_dir(src, dst, only_new=False, only_recent=False, excludes=['.*'], inclu
|
|||||||
copy_file(src, dst, only_new=only_new, only_recent=only_recent)
|
copy_file(src, dst, only_new=only_new, only_recent=only_recent)
|
||||||
|
|
||||||
elif src.is_dir():
|
elif src.is_dir():
|
||||||
src_files = list(src.rglob('*'))
|
src_files = list(src.rglob("*"))
|
||||||
if excludes:
|
if excludes:
|
||||||
src_files = [f for f in src_files if not re.match(excludes, f.name)]
|
src_files = [f for f in src_files if not re.match(excludes, f.name)]
|
||||||
|
|
||||||
if includes:
|
if includes:
|
||||||
src_files = [f for f in src_files if re.match(includes, f.name)]
|
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():
|
if src_file.is_dir():
|
||||||
dst_file.mkdir(exist_ok=True, parents=True)
|
dst_file.mkdir(exist_ok=True, parents=True)
|
||||||
else:
|
else:
|
||||||
copy_file(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):
|
def open_file(filepath, select=False):
|
||||||
'''Open a filepath inside the os explorer'''
|
"""Open a filepath inside the os explorer"""
|
||||||
|
|
||||||
if platform.system() == 'Darwin': # macOS
|
if platform.system() == "Darwin": # macOS
|
||||||
cmd = ['open']
|
cmd = ["open"]
|
||||||
elif platform.system() == 'Windows': # Windows
|
elif platform.system() == "Windows": # Windows
|
||||||
cmd = ['explorer']
|
cmd = ["explorer"]
|
||||||
if select:
|
if select:
|
||||||
cmd += ['/select,']
|
cmd += ["/select,"]
|
||||||
else: # linux variants
|
else: # linux variants
|
||||||
cmd = ['xdg-open']
|
cmd = ["xdg-open"]
|
||||||
if select:
|
if select:
|
||||||
cmd = ['nemo']
|
cmd = ["nemo"]
|
||||||
|
|
||||||
cmd += [str(filepath)]
|
cmd += [str(filepath)]
|
||||||
subprocess.Popen(cmd)
|
subprocess.Popen(cmd)
|
||||||
@@ -221,8 +245,8 @@ def open_blender_file(filepath=None):
|
|||||||
cmd = sys.argv
|
cmd = sys.argv
|
||||||
|
|
||||||
# if no filepath, use command as is to reopen blender
|
# if no filepath, use command as is to reopen blender
|
||||||
if filepath != '':
|
if filepath != "":
|
||||||
if len(cmd) > 1 and cmd[1].endswith('.blend'):
|
if len(cmd) > 1 and cmd[1].endswith(".blend"):
|
||||||
cmd[1] = str(filepath)
|
cmd[1] = str(filepath)
|
||||||
else:
|
else:
|
||||||
cmd.insert(1, str(filepath))
|
cmd.insert(1, str(filepath))
|
||||||
@@ -230,63 +254,38 @@ def open_blender_file(filepath=None):
|
|||||||
subprocess.Popen(cmd)
|
subprocess.Popen(cmd)
|
||||||
|
|
||||||
|
|
||||||
def cache(timeout):
|
|
||||||
_cache = {}
|
|
||||||
|
|
||||||
def decorator(func):
|
|
||||||
@wraps(func)
|
|
||||||
def wrapper(*args, **kwargs):
|
|
||||||
current_time = perf_counter()
|
|
||||||
cache_key = (*args, *kwargs.items()) # Assuming the first argument is the file path
|
|
||||||
|
|
||||||
# Check if the cache is valid
|
|
||||||
if cache_key in _cache:
|
|
||||||
cached_content, cache_time = _cache[cache_key]
|
|
||||||
if (current_time - cache_time) < timeout:
|
|
||||||
|
|
||||||
return cached_content
|
|
||||||
|
|
||||||
# Execute the function and update the cache
|
|
||||||
result = func(*args, **kwargs)
|
|
||||||
_cache[cache_key] = (result, current_time)
|
|
||||||
return result
|
|
||||||
|
|
||||||
return wrapper
|
|
||||||
return decorator
|
|
||||||
|
|
||||||
|
|
||||||
def read_file(path):
|
def read_file(path):
|
||||||
'''Read a file with an extension in (json, yaml, yml, txt)'''
|
"""Read a file with an extension in (json, yaml, yml, txt)"""
|
||||||
|
|
||||||
exts = ('.json', '.yaml', '.yml', '.txt')
|
exts = (".json", ".yaml", ".yml", ".txt")
|
||||||
|
|
||||||
if not path:
|
if not path:
|
||||||
print('Try to read empty file')
|
print("Try to read empty file")
|
||||||
|
|
||||||
path = Path(path)
|
path = Path(path)
|
||||||
if not path.exists():
|
if not path.exists():
|
||||||
print('File not exist', path)
|
print("File not exist", path)
|
||||||
return
|
return
|
||||||
|
|
||||||
if path.suffix not in exts:
|
if path.suffix not in exts:
|
||||||
print(f'Cannot read file {path}, extension must be in {exts}')
|
print(f"Cannot read file {path}, extension must be in {exts}")
|
||||||
return
|
return
|
||||||
|
|
||||||
txt = path.read_text()
|
txt = path.read_text()
|
||||||
data = None
|
data = None
|
||||||
|
|
||||||
if path.suffix.lower() in ('.yaml', '.yml'):
|
if path.suffix.lower() in (".yaml", ".yml"):
|
||||||
yaml = install_module('yaml')
|
yaml = install_module("yaml")
|
||||||
try:
|
try:
|
||||||
data = yaml.safe_load(txt)
|
data = yaml.safe_load(txt)
|
||||||
except Exception:
|
except Exception:
|
||||||
print(f'Could not load yaml file {path}')
|
print(f"Could not load yaml file {path}")
|
||||||
return
|
return
|
||||||
elif path.suffix.lower() == '.json':
|
elif path.suffix.lower() == ".json":
|
||||||
try:
|
try:
|
||||||
data = json.loads(txt)
|
data = json.loads(txt)
|
||||||
except Exception:
|
except Exception:
|
||||||
print(f'Could not load json file {path}')
|
print(f"Could not load json file {path}")
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
data = txt
|
data = txt
|
||||||
@@ -295,63 +294,66 @@ def read_file(path):
|
|||||||
|
|
||||||
|
|
||||||
def write_file(path, data, indent=4):
|
def write_file(path, data, indent=4):
|
||||||
'''Read a file with an extension in (json, yaml, yml, text)'''
|
"""Read a file with an extension in (json, yaml, yml, text)"""
|
||||||
|
|
||||||
exts = ('.json', '.yaml', '.yml', '.txt')
|
exts = (".json", ".yaml", ".yml", ".txt")
|
||||||
|
|
||||||
if not path:
|
if not path:
|
||||||
print('Try to write empty file')
|
print("Try to write empty file")
|
||||||
|
|
||||||
path = Path(path)
|
path = Path(path)
|
||||||
path.parent.mkdir(parents=True, exist_ok=True)
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
if path.suffix not in exts:
|
if path.suffix not in exts:
|
||||||
print(f'Cannot read file {path}, extension must be in {exts}')
|
print(f"Cannot read file {path}, extension must be in {exts}")
|
||||||
return
|
return
|
||||||
|
|
||||||
if path.suffix.lower() in ('.yaml', '.yml'):
|
if path.suffix.lower() in (".yaml", ".yml"):
|
||||||
yaml = install_module('yaml')
|
yaml = install_module("yaml")
|
||||||
try:
|
try:
|
||||||
path.write_text(yaml.dump(data), encoding='utf8')
|
path.write_text(yaml.dump(data), encoding="utf8")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
print(e)
|
||||||
print(f'Could not write yaml file {path}')
|
print(f"Could not write yaml file {path}")
|
||||||
return
|
return
|
||||||
elif path.suffix.lower() == '.json':
|
elif path.suffix.lower() == ".json":
|
||||||
try:
|
try:
|
||||||
path.write_text(json.dumps(data, indent=indent), encoding='utf8')
|
path.write_text(json.dumps(data, indent=indent), encoding="utf8")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
print(e)
|
||||||
print(f'Could not write json file {path}')
|
print(f"Could not write json file {path}")
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
data = path.write_text(data, encoding='utf8')
|
data = path.write_text(data, encoding="utf8")
|
||||||
|
|
||||||
|
|
||||||
def synchronize(src, dst, only_new=False, only_recent=False, clear=False):
|
def synchronize(src, dst, only_new=False, only_recent=False, clear=False):
|
||||||
|
|
||||||
#actionlib_dir = get_actionlib_dir(custom=custom)
|
# actionlib_dir = get_actionlib_dir(custom=custom)
|
||||||
#local_actionlib_dir = get_actionlib_dir(local=True, custom=custom)
|
# local_actionlib_dir = get_actionlib_dir(local=True, custom=custom)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if clear and Path(dst).exists():
|
if clear and Path(dst).exists():
|
||||||
shutil.rmtree(dst)
|
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 = [
|
cmd = [
|
||||||
sys.executable,
|
sys.executable,
|
||||||
script,
|
script,
|
||||||
'--src', str(src),
|
"--src",
|
||||||
'--dst', str(dst),
|
str(src),
|
||||||
'--only-new', json.dumps(only_new),
|
"--dst",
|
||||||
'--only-recent', json.dumps(only_recent),
|
str(dst),
|
||||||
|
"--only-new",
|
||||||
|
json.dumps(only_new),
|
||||||
|
"--only-recent",
|
||||||
|
json.dumps(only_recent),
|
||||||
]
|
]
|
||||||
|
|
||||||
subprocess.Popen(cmd)
|
subprocess.Popen(cmd)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
print(e)
|
||||||
|
|
||||||
@@ -0,0 +1,462 @@
|
|||||||
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
Function relative to the asset browser addon
|
||||||
|
"""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
|
import time
|
||||||
|
|
||||||
|
# from asset_library.constants import ASSETLIB_FILENAME
|
||||||
|
import inspect
|
||||||
|
from asset_library.common.file_utils import read_file
|
||||||
|
from asset_library.common.bl_utils import get_addon_prefs
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
import bpy
|
||||||
|
|
||||||
|
|
||||||
|
def command(func):
|
||||||
|
"""Decorator to be used from printed functions argument and run time"""
|
||||||
|
func_name = func.__name__.replace("_", " ").title()
|
||||||
|
|
||||||
|
def _command(*args, **kargs):
|
||||||
|
|
||||||
|
bound = inspect.signature(func).bind(*args, **kargs)
|
||||||
|
bound.apply_defaults()
|
||||||
|
|
||||||
|
args_str = ", ".join([f"{k}={v}" for k, v in bound.arguments.items()])
|
||||||
|
print(f"\n[>-] {func_name} ({args_str}) --- Start ---")
|
||||||
|
|
||||||
|
t0 = time.time()
|
||||||
|
result = func(*args, **kargs)
|
||||||
|
|
||||||
|
print(
|
||||||
|
f"[>-] {func_name} --- Finished (total time : {time.time() - t0:.2f}s) ---"
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
|
||||||
|
return _command
|
||||||
|
|
||||||
|
|
||||||
|
def asset_warning_callback(self, context):
|
||||||
|
"""Callback function to display a warning message when ading or modifying an asset"""
|
||||||
|
self.warning = ""
|
||||||
|
|
||||||
|
if not self.name:
|
||||||
|
self.warning = "You need to specify a name"
|
||||||
|
return
|
||||||
|
if not self.catalog:
|
||||||
|
self.warning = "You need to specify a catalog"
|
||||||
|
return
|
||||||
|
|
||||||
|
lib = get_active_library()
|
||||||
|
action_path = lib.library_type.get_asset_relative_path(self.name, self.catalog)
|
||||||
|
self.path = action_path.as_posix()
|
||||||
|
|
||||||
|
if lib.merge_libraries:
|
||||||
|
prefs = get_addon_prefs()
|
||||||
|
lib = prefs.libraries[lib.store_library]
|
||||||
|
|
||||||
|
if not lib.library_type.get_asset_path(self.name, self.catalog).parents[1].exists():
|
||||||
|
self.warning = "A new folder will be created"
|
||||||
|
|
||||||
|
|
||||||
|
def get_active_library():
|
||||||
|
"""Get the pref library properties from the active library of the asset browser"""
|
||||||
|
prefs = get_addon_prefs()
|
||||||
|
asset_lib_ref = bpy.context.space_data.params.asset_library_ref
|
||||||
|
|
||||||
|
# Check for merged library
|
||||||
|
for l in prefs.libraries:
|
||||||
|
if l.library_name == asset_lib_ref:
|
||||||
|
return l
|
||||||
|
|
||||||
|
|
||||||
|
def get_active_catalog():
|
||||||
|
"""Get the active catalog path"""
|
||||||
|
|
||||||
|
lib = get_active_library()
|
||||||
|
cat_data = lib.library_type.read_catalog()
|
||||||
|
cat_data = {v["id"]: k for k, v in cat_data.items()}
|
||||||
|
|
||||||
|
cat_id = bpy.context.space_data.params.catalog_id
|
||||||
|
if cat_id in cat_data:
|
||||||
|
return cat_data[cat_id]
|
||||||
|
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
def norm_asset_datas(asset_file_datas):
|
||||||
|
''' Return a new flat list of asset data
|
||||||
|
the filepath keys are merge with the assets keys'''
|
||||||
|
|
||||||
|
asset_datas = []
|
||||||
|
for asset_file_data in asset_file_datas:
|
||||||
|
asset_file_data = asset_file_data.copy()
|
||||||
|
if 'assets' in asset_file_data:
|
||||||
|
|
||||||
|
assets = asset_file_data.pop('assets')
|
||||||
|
for asset_data in assets:
|
||||||
|
|
||||||
|
asset_datas.append({**asset_file_data, **asset_data})
|
||||||
|
|
||||||
|
else:
|
||||||
|
asset_datas.append(asset_file_data)
|
||||||
|
|
||||||
|
return asset_datas
|
||||||
|
|
||||||
|
def cache_diff(cache, new_cache):
|
||||||
|
'''Compare and return the difference between two asset datas list'''
|
||||||
|
|
||||||
|
#TODO use an id to be able to tell modified asset if renamed
|
||||||
|
#cache = {a.get('id', a['name']) : a for a in norm_asset_datas(cache)}
|
||||||
|
#new_cache = {a.get('id', a['name']) : a for a in norm_asset_datas(new_cache)}
|
||||||
|
|
||||||
|
cache = {f"{a['filepath']}/{a['name']}": a for a in norm_asset_datas(cache)}
|
||||||
|
new_cache = {f"{a['filepath']}/{a['name']}" : a for a in norm_asset_datas(new_cache)}
|
||||||
|
|
||||||
|
assets_added = [v for k, v in new_cache.items() if k not in cache]
|
||||||
|
assets_removed = [v for k, v in cache.items() if k not in new_cache]
|
||||||
|
assets_modified = [v for k, v in cache.items() if v not in assets_removed and v!= new_cache[k]]
|
||||||
|
|
||||||
|
if assets_added:
|
||||||
|
print(f'{len(assets_added)} Assets Added \n{tuple(a["name"] for a in assets_added[:10])}\n')
|
||||||
|
if assets_removed:
|
||||||
|
print(f'{len(assets_removed)} Assets Removed \n{tuple(a["name"] for a in assets_removed[:10])}\n')
|
||||||
|
if assets_modified:
|
||||||
|
print(f'{len(assets_modified)} Assets Modified \n{tuple(a["name"] for a in assets_modified[:10])}\n')
|
||||||
|
|
||||||
|
assets_added = [dict(a, operation='ADD') for a in assets_added]
|
||||||
|
assets_removed = [dict(a, operation='REMOVE') for a in assets_removed]
|
||||||
|
assets_modified = [dict(a, operation='MODIFY') for a in assets_modified]
|
||||||
|
|
||||||
|
assets_diff = assets_added + assets_removed + assets_modified
|
||||||
|
if not assets_diff:
|
||||||
|
print('No change in the library')
|
||||||
|
|
||||||
|
return assets_diff
|
||||||
|
|
||||||
|
def clean_default_lib():
|
||||||
|
prefs = bpy.context.preferences
|
||||||
|
|
||||||
|
if not prefs.filepaths.asset_libraries:
|
||||||
|
print('[>-] No Asset Libraries Filepaths Setted.')
|
||||||
|
return
|
||||||
|
|
||||||
|
lib, lib_id = get_lib_id(
|
||||||
|
library_name='User Library',
|
||||||
|
asset_libraries=prefs.filepaths.asset_libraries
|
||||||
|
)
|
||||||
|
if lib:
|
||||||
|
bpy.ops.preferences.asset_library_remove(index=lib_id)
|
||||||
|
|
||||||
|
def get_asset_source(replace_local=False):
|
||||||
|
sp = bpy.context.space_data
|
||||||
|
prefs = bpy.context.preferences.addons[__package__].preferences
|
||||||
|
asset_file_handle = bpy.context.asset_file_handle
|
||||||
|
|
||||||
|
if asset_file_handle is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if asset_file_handle.local_id:
|
||||||
|
publish_path = os.path.expandvars(scn.actionlib.get('publish_path'))
|
||||||
|
if not publish_path:
|
||||||
|
print('[>.] No \'Publish Dir\' found. Publish file first.' )
|
||||||
|
return None
|
||||||
|
|
||||||
|
return Path(publish_path)
|
||||||
|
|
||||||
|
asset_library_ref = bpy.context.asset_library_ref
|
||||||
|
source_path = bpy.types.AssetHandle.get_full_library_path(asset_file_handle, asset_library_ref)
|
||||||
|
|
||||||
|
if replace_local:
|
||||||
|
if 'custom' in sp.params.asset_library_ref.lower():
|
||||||
|
actionlib_path = prefs.action.custom_path
|
||||||
|
actionlib_path_local = prefs.action.custom_path_local
|
||||||
|
else:
|
||||||
|
actionlib_path = prefs.action.path
|
||||||
|
actionlib_path_local = prefs.action.path_local
|
||||||
|
|
||||||
|
source_path = re.sub(actionlib_dir_local, actionlib_dir, source_path)
|
||||||
|
|
||||||
|
return source_path
|
||||||
|
"""
|
||||||
|
"""
|
||||||
|
def get_catalog_path(filepath=None):
|
||||||
|
filepath = filepath or bpy.data.filepath
|
||||||
|
filepath = Path(filepath)
|
||||||
|
|
||||||
|
if filepath.is_file():
|
||||||
|
filepath = filepath.parent
|
||||||
|
|
||||||
|
filepath.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
catalog = filepath / 'blender_assets.cats.txt'
|
||||||
|
if not catalog.exists():
|
||||||
|
catalog.touch(exist_ok=False)
|
||||||
|
|
||||||
|
return catalog
|
||||||
|
"""
|
||||||
|
|
||||||
|
# def read_catalog(path, key='path'):
|
||||||
|
# cat_data = {}
|
||||||
|
|
||||||
|
# supported_keys = ('path', 'id', 'name')
|
||||||
|
|
||||||
|
# if key not in supported_keys:
|
||||||
|
# raise Exception(f'Not supported key: {key} for read catalog, supported keys are {supported_keys}')
|
||||||
|
|
||||||
|
# for line in Path(path).read_text(encoding="utf-8").split('\n'):
|
||||||
|
# if line.startswith(('VERSION', '#')) or not line:
|
||||||
|
# continue
|
||||||
|
|
||||||
|
# cat_id, cat_path, cat_name = line.split(':')
|
||||||
|
|
||||||
|
# if key == 'id':
|
||||||
|
# cat_data[cat_id] = {'path':cat_path, 'name':cat_name}
|
||||||
|
# elif key == 'path':
|
||||||
|
# cat_data[cat_path] = {'id':cat_id, 'name':cat_name}
|
||||||
|
# elif key =='name':
|
||||||
|
# cat_data[cat_name] = {'id':cat_id, 'path':cat_path}
|
||||||
|
|
||||||
|
# return cat_data
|
||||||
|
"""
|
||||||
|
def read_catalog(path):
|
||||||
|
cat_data = {}
|
||||||
|
|
||||||
|
for line in Path(path).read_text(encoding="utf-8").split('\n'):
|
||||||
|
if line.startswith(('VERSION', '#')) or not line:
|
||||||
|
continue
|
||||||
|
|
||||||
|
cat_id, cat_path, cat_name = line.split(':')
|
||||||
|
cat_data[cat_path] = {'id':cat_id, 'name':cat_name}
|
||||||
|
|
||||||
|
return cat_data
|
||||||
|
|
||||||
|
def write_catalog(path, data):
|
||||||
|
lines = ['VERSION 1', '']
|
||||||
|
|
||||||
|
# Add missing parents catalog
|
||||||
|
norm_data = {}
|
||||||
|
for cat_path, cat_data in data.items():
|
||||||
|
norm_data[cat_path] = cat_data
|
||||||
|
for p in Path(cat_path).parents[:-1]:
|
||||||
|
if p in data or p in norm_data:
|
||||||
|
continue
|
||||||
|
|
||||||
|
norm_data[p.as_posix()] = {'id': str(uuid.uuid4()), 'name': '-'.join(p.parts)}
|
||||||
|
|
||||||
|
for cat_path, cat_data in sorted(norm_data.items()):
|
||||||
|
cat_name = cat_data['name'].replace('/', '-')
|
||||||
|
lines.append(f"{cat_data['id']}:{cat_path}:{cat_name}")
|
||||||
|
|
||||||
|
print(f'Catalog writen at: {path}')
|
||||||
|
Path(path).write_text('\n'.join(lines), encoding="utf-8")
|
||||||
|
|
||||||
|
def create_catalog_file(json_path : str|Path, keep_existing_category : bool = True):
|
||||||
|
'''create asset catalog file from json
|
||||||
|
if catalog already exists, keep existing catalog uid'''
|
||||||
|
|
||||||
|
json_path = Path(json_path)
|
||||||
|
# if not json.exists(): return
|
||||||
|
assert json_path.exists(), 'Json not exists !'
|
||||||
|
|
||||||
|
category_datas = json.loads(json_path.read_text(encoding="utf-8"))
|
||||||
|
|
||||||
|
catalog_path = json_path.parent / 'blender_assets.cats.txt'
|
||||||
|
catalog_data = {}
|
||||||
|
if catalog_path.exists():
|
||||||
|
catalog_data = read_catalog(catalog_path)
|
||||||
|
## retrun a format catalog_data[path] = {'id':id, 'name':name}
|
||||||
|
## note: 'path' in catalog is 'name' in category_datas
|
||||||
|
|
||||||
|
catalog_lines = ['VERSION 1', '']
|
||||||
|
|
||||||
|
## keep existing
|
||||||
|
for c in category_datas:
|
||||||
|
# keep same catalog line for existing category keys
|
||||||
|
if keep_existing_category and catalog_data.get(c['name']):
|
||||||
|
print(c['name'], 'category exists')
|
||||||
|
cat = catalog_data[c['name']] #get
|
||||||
|
catalog_lines.append(f"{cat['id']}:{c['name']}:{cat['name']}")
|
||||||
|
else:
|
||||||
|
print(c['name'], 'new category')
|
||||||
|
# add new category
|
||||||
|
catalog_lines.append(f"{c['id']}:{c['name']}:{c['name'].replace('/', '-')}")
|
||||||
|
|
||||||
|
## keep category that are non-existing in json ?
|
||||||
|
if keep_existing_category:
|
||||||
|
for k in catalog_data.keys():
|
||||||
|
if next((c['name'] for c in category_datas if c['name'] == k), None):
|
||||||
|
continue
|
||||||
|
print(k, 'category not existing in json')
|
||||||
|
cat = catalog_data[k]
|
||||||
|
# rebuild existing line
|
||||||
|
catalog_lines.append(f"{cat['id']}:{k}:{cat['name']}")
|
||||||
|
|
||||||
|
## write_text overwrite the file
|
||||||
|
catalog_path.write_text('\n'.join(catalog_lines), encoding="utf-8")
|
||||||
|
|
||||||
|
print(f'Catalog saved at: {catalog_path}')
|
||||||
|
|
||||||
|
return
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def clear_env_libraries():
|
||||||
|
print("clear_env_libraries")
|
||||||
|
|
||||||
|
prefs = get_addon_prefs()
|
||||||
|
asset_libraries = bpy.context.preferences.filepaths.asset_libraries
|
||||||
|
|
||||||
|
for env_lib in prefs.env_libraries:
|
||||||
|
name = env_lib.get("asset_library")
|
||||||
|
if not name:
|
||||||
|
continue
|
||||||
|
|
||||||
|
asset_lib = asset_libraries.get(name)
|
||||||
|
if not asset_lib:
|
||||||
|
continue
|
||||||
|
|
||||||
|
index = list(asset_libraries).index(asset_lib)
|
||||||
|
bpy.ops.preferences.asset_library_remove(index=index)
|
||||||
|
|
||||||
|
prefs.env_libraries.clear()
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
env_libs = get_env_libraries()
|
||||||
|
paths = [Path(l['path']).resolve().as_posix() for n, l in env_libs.items()]
|
||||||
|
|
||||||
|
for i, l in reversed(enumerate(libs)):
|
||||||
|
lib_path = Path(l.path).resolve().as_posix()
|
||||||
|
|
||||||
|
if (l.name in env_libs or lib_path in paths):
|
||||||
|
libs.remove(i)
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def set_env_libraries(path=None) -> list:
|
||||||
|
"""Read the environments variables and create the libraries"""
|
||||||
|
|
||||||
|
# from asset_library.prefs import AssetLibraryOptions
|
||||||
|
prefs = get_addon_prefs()
|
||||||
|
path = path or prefs.config_directory
|
||||||
|
|
||||||
|
# print('Read', path)
|
||||||
|
library_data = read_file(path)
|
||||||
|
|
||||||
|
clear_env_libraries()
|
||||||
|
|
||||||
|
if not library_data:
|
||||||
|
return
|
||||||
|
|
||||||
|
libs = []
|
||||||
|
|
||||||
|
for lib_info in library_data:
|
||||||
|
lib = prefs.env_libraries.add()
|
||||||
|
|
||||||
|
lib.set_dict(lib_info)
|
||||||
|
|
||||||
|
libs.append(lib)
|
||||||
|
|
||||||
|
return libs
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
def get_env_libraries():
|
||||||
|
env_libraries = {}
|
||||||
|
|
||||||
|
for k, v in os.environ.items():
|
||||||
|
if not re.findall('ASSET_LIBRARY_[0-9]', k):
|
||||||
|
continue
|
||||||
|
|
||||||
|
lib_infos = v.split(os.pathsep)
|
||||||
|
|
||||||
|
if len(lib_infos) == 5:
|
||||||
|
name, data_type, tpl, src_path, bdl_path = lib_infos
|
||||||
|
elif len(lib_infos) == 4:
|
||||||
|
name, data_type, tpl, src_path = lib_infos
|
||||||
|
bdl_path = ''
|
||||||
|
else:
|
||||||
|
print(f'Wrong env key {k}', lib_infos)
|
||||||
|
continue
|
||||||
|
|
||||||
|
source_type = 'TEMPLATE'
|
||||||
|
if tpl.lower().endswith(('.json', '.yml', 'yaml')):
|
||||||
|
source_type = 'DATA_FILE'
|
||||||
|
|
||||||
|
env_libraries[name] = {
|
||||||
|
'data_type': data_type,
|
||||||
|
'source_directory': src_path,
|
||||||
|
'bundle_directory': bdl_path,
|
||||||
|
'source_type': source_type,
|
||||||
|
'template': tpl,
|
||||||
|
}
|
||||||
|
|
||||||
|
return env_libraries
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def resync_lib(name, waiting_time):
|
||||||
|
bpy.app.timers.register(
|
||||||
|
lambda: bpy.ops.assetlib.synchronize(only_recent=True, name=name),
|
||||||
|
first_interval=waiting_time,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
def set_assetlib_paths():
|
||||||
|
prefs = bpy.context.preferences
|
||||||
|
|
||||||
|
assetlib_name = 'Assets'
|
||||||
|
assetlib = prefs.filepaths.asset_libraries.get(assetlib_name)
|
||||||
|
|
||||||
|
if not assetlib:
|
||||||
|
bpy.ops.preferences.asset_library_add(directory=str(assetlib_path))
|
||||||
|
assetlib = prefs.filepaths.asset_libraries[-1]
|
||||||
|
assetlib.name = assetlib_name
|
||||||
|
|
||||||
|
assetlib.path = str(actionlib_dir)
|
||||||
|
|
||||||
|
def set_actionlib_paths():
|
||||||
|
prefs = bpy.context.preferences
|
||||||
|
|
||||||
|
actionlib_name = 'Action Library'
|
||||||
|
actionlib_custom_name = 'Action Library Custom'
|
||||||
|
|
||||||
|
actionlib = prefs.filepaths.asset_libraries.get(actionlib_name)
|
||||||
|
|
||||||
|
if not assetlib:
|
||||||
|
bpy.ops.preferences.asset_library_add(directory=str(assetlib_path))
|
||||||
|
assetlib = prefs.filepaths.asset_libraries[-1]
|
||||||
|
assetlib.name = assetlib_name
|
||||||
|
|
||||||
|
actionlib_dir = get_actionlib_dir(custom=custom)
|
||||||
|
local_actionlib_dir = get_actionlib_dir(local=True, custom=custom)
|
||||||
|
|
||||||
|
if local_actionlib_dir:
|
||||||
|
actionlib_dir = local_actionlib_dir
|
||||||
|
|
||||||
|
if actionlib_name not in prefs.filepaths.asset_libraries:
|
||||||
|
bpy.ops.preferences.asset_library_add(directory=str(actionlib_dir))
|
||||||
|
|
||||||
|
#lib, lib_id = get_lib_id(
|
||||||
|
# library_path=actionlib_dir,
|
||||||
|
# asset_libraries=prefs.filepaths.asset_libraries
|
||||||
|
#)
|
||||||
|
|
||||||
|
#if not lib:
|
||||||
|
# print(f'Cannot set dir for {actionlib_name}')
|
||||||
|
# return
|
||||||
|
|
||||||
|
prefs.filepaths.asset_libraries[lib_id].name = actionlib_name
|
||||||
|
#prefs.filepaths.asset_libraries[lib_id].path = str(actionlib_dir)
|
||||||
|
"""
|
||||||
@@ -0,0 +1,394 @@
|
|||||||
|
import bpy
|
||||||
|
from pathlib import Path
|
||||||
|
from asset_library.common.file_utils import read_file, write_file
|
||||||
|
from copy import deepcopy
|
||||||
|
import time
|
||||||
|
from itertools import groupby
|
||||||
|
|
||||||
|
|
||||||
|
class AssetCache:
|
||||||
|
def __init__(self, file_cache, data=None):
|
||||||
|
|
||||||
|
self.file_cache = file_cache
|
||||||
|
|
||||||
|
self.catalog = None
|
||||||
|
self.author = None
|
||||||
|
self.description = None
|
||||||
|
self.tags = None
|
||||||
|
self.type = None
|
||||||
|
self.name = None
|
||||||
|
self._metadata = None
|
||||||
|
|
||||||
|
if data:
|
||||||
|
self.set_data(data)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def filepath(self):
|
||||||
|
return self.file_cache.filepath
|
||||||
|
|
||||||
|
@property
|
||||||
|
def library_id(self):
|
||||||
|
return self.file_cache.library_id
|
||||||
|
|
||||||
|
@property
|
||||||
|
def metadata(self):
|
||||||
|
metadata = {".library_id": self.library_id, ".filepath": self.filepath}
|
||||||
|
|
||||||
|
metadata.update(self._metadata)
|
||||||
|
|
||||||
|
return metadata
|
||||||
|
|
||||||
|
@property
|
||||||
|
def norm_name(self):
|
||||||
|
return self.name.replace(" ", "_").lower()
|
||||||
|
|
||||||
|
def unique_name(self):
|
||||||
|
return (self.filepath / self.name).as_posix()
|
||||||
|
|
||||||
|
def set_data(self, data):
|
||||||
|
catalog = data["catalog"]
|
||||||
|
if isinstance(catalog, (list, tuple)):
|
||||||
|
catalog = "/".join(catalog)
|
||||||
|
|
||||||
|
self.catalog = catalog
|
||||||
|
self.author = data.get("author", "")
|
||||||
|
self.description = data.get("description", "")
|
||||||
|
self.tags = data.get("tags", [])
|
||||||
|
self.type = data.get("type")
|
||||||
|
self.name = data["name"]
|
||||||
|
self._metadata = data.get("metadata", {})
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
return dict(
|
||||||
|
catalog=self.catalog,
|
||||||
|
author=self.author,
|
||||||
|
metadata=self.metadata,
|
||||||
|
description=self.description,
|
||||||
|
tags=self.tags,
|
||||||
|
type=self.type,
|
||||||
|
name=self.name,
|
||||||
|
)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"AssetCache(name={self.name}, catalog={self.catalog})"
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return self.to_dict() == other.to_dict()
|
||||||
|
|
||||||
|
|
||||||
|
class AssetsCache:
|
||||||
|
def __init__(self, file_cache):
|
||||||
|
|
||||||
|
self.file_cache = file_cache
|
||||||
|
self._data = []
|
||||||
|
|
||||||
|
def add(self, asset_cache_data, **kargs):
|
||||||
|
asset_cache = AssetCache(self.file_cache, {**asset_cache_data, **kargs})
|
||||||
|
self._data.append(asset_cache)
|
||||||
|
|
||||||
|
return asset_cache
|
||||||
|
|
||||||
|
def remove(self, asset_cache):
|
||||||
|
if isinstance(asset_cache, str):
|
||||||
|
asset_cache = self.get(asset_cache)
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
return self._data.__iter__()
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
if isinstance(key, str):
|
||||||
|
return self.to_dict()[key]
|
||||||
|
else:
|
||||||
|
return self._data[key]
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
return {a.name: a for a in self}
|
||||||
|
|
||||||
|
def get(self, name):
|
||||||
|
return next((a for a in self if a.name == name), None)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"AssetsCache({list(self)})"
|
||||||
|
|
||||||
|
|
||||||
|
class FileCache:
|
||||||
|
def __init__(self, library_cache, data=None):
|
||||||
|
|
||||||
|
self.library_cache = library_cache
|
||||||
|
|
||||||
|
self.filepath = None
|
||||||
|
self.modified = None
|
||||||
|
self.assets = AssetsCache(self)
|
||||||
|
|
||||||
|
if data:
|
||||||
|
self.set_data(data)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def library_id(self):
|
||||||
|
return self.library_cache.library_id
|
||||||
|
|
||||||
|
def set_data(self, data):
|
||||||
|
|
||||||
|
if "filepath" in data:
|
||||||
|
self.filepath = Path(data["filepath"])
|
||||||
|
|
||||||
|
self.modified = data.get("modified", time.time_ns())
|
||||||
|
|
||||||
|
if data.get("type") == "FILE":
|
||||||
|
self.assets.add(data)
|
||||||
|
|
||||||
|
for asset_cache_data in data.get("assets", []):
|
||||||
|
self.assets.add(asset_cache_data)
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
return dict(
|
||||||
|
filepath=self.filepath.as_posix(),
|
||||||
|
modified=self.modified,
|
||||||
|
library_id=self.library_id,
|
||||||
|
assets=[asset_cache.to_dict() for asset_cache in self],
|
||||||
|
)
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
return self.assets.__iter__()
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
return self._data[key]
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"FileCache(filepath={self.filepath})"
|
||||||
|
|
||||||
|
|
||||||
|
class AssetCacheDiff:
|
||||||
|
def __init__(self, library_cache, asset_cache, operation):
|
||||||
|
|
||||||
|
self.library_cache = library_cache
|
||||||
|
# self.filepath = data['filepath']
|
||||||
|
self.operation = operation
|
||||||
|
self.asset_cache = asset_cache
|
||||||
|
|
||||||
|
|
||||||
|
class LibraryCacheDiff:
|
||||||
|
def __init__(self, old_cache=None, new_cache=None, filepath=None):
|
||||||
|
|
||||||
|
self.filepath = filepath
|
||||||
|
self._data = []
|
||||||
|
|
||||||
|
self.compare(old_cache, new_cache)
|
||||||
|
|
||||||
|
def add(self, asset_cache_datas, operation):
|
||||||
|
if not isinstance(asset_cache_datas, (list, tuple)):
|
||||||
|
asset_cache_datas = [asset_cache_datas]
|
||||||
|
|
||||||
|
new_asset_diffs = []
|
||||||
|
for cache_data in asset_cache_datas:
|
||||||
|
new_asset_diffs.append(AssetCacheDiff(self, cache_data, operation))
|
||||||
|
|
||||||
|
self._data += new_asset_diffs
|
||||||
|
|
||||||
|
return new_asset_diffs
|
||||||
|
|
||||||
|
def compare(self, old_cache, new_cache):
|
||||||
|
if old_cache is None or new_cache is None:
|
||||||
|
print("Cannot Compare cache with None")
|
||||||
|
|
||||||
|
cache_dict = {a.unique_name: a for a in old_cache.asset_caches}
|
||||||
|
new_cache_dict = {a.unique_name: a for a in new_cache.asset_caches}
|
||||||
|
|
||||||
|
assets_added = self.add(
|
||||||
|
[v for k, v in new_cache_dict.items() if k not in cache_dict], "ADD"
|
||||||
|
)
|
||||||
|
assets_removed = self.add(
|
||||||
|
[v for k, v in cache_dict.items() if k not in new_cache_dict], "REMOVED"
|
||||||
|
)
|
||||||
|
assets_modified = self.add(
|
||||||
|
[
|
||||||
|
v
|
||||||
|
for k, v in cache_dict.items()
|
||||||
|
if v not in assets_removed and v != new_cache_dict[k]
|
||||||
|
],
|
||||||
|
"MODIFIED",
|
||||||
|
)
|
||||||
|
|
||||||
|
if assets_added:
|
||||||
|
print(
|
||||||
|
f"{len(assets_added)} Assets Added \n{tuple(a.name for a in assets_added[:10])}...\n"
|
||||||
|
)
|
||||||
|
if assets_removed:
|
||||||
|
print(
|
||||||
|
f"{len(assets_removed)} Assets Removed \n{tuple(a.name for a in assets_removed[:10])}...\n"
|
||||||
|
)
|
||||||
|
if assets_modified:
|
||||||
|
print(
|
||||||
|
f"{len(assets_modified)} Assets Modified \n{tuple(a.name for a in assets_modified[:10])}...\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
if len(self) == 0:
|
||||||
|
print("No change in the library")
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
|
def group_by(self, key):
|
||||||
|
"""Return groups of file cache diff using the key provided"""
|
||||||
|
data = list(self).sort(key=key)
|
||||||
|
return groupby(data, key=key)
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
return iter(self._data)
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
return self._data[key]
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
return len(self._data)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"LibraryCacheDiff(operations={[o for o in self][:2]}...)"
|
||||||
|
|
||||||
|
|
||||||
|
class LibraryCache:
|
||||||
|
def __init__(self, filepath):
|
||||||
|
|
||||||
|
self.filepath = Path(filepath)
|
||||||
|
self._data = []
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_library(cls, library):
|
||||||
|
filepath = library.library_path / f"blender_assets.{library.id}.json"
|
||||||
|
return cls(filepath)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def filename(self):
|
||||||
|
return self.filepath.name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def library_id(self):
|
||||||
|
return self.filepath.stem.split(".")[-1]
|
||||||
|
|
||||||
|
# @property
|
||||||
|
# def filepath(self):
|
||||||
|
# """Get the filepath of the library json file relative to the library"""
|
||||||
|
# return self.directory / self.filename
|
||||||
|
|
||||||
|
def catalogs(self):
|
||||||
|
return set(a.catalog for a in self.asset_caches)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def asset_caches(self):
|
||||||
|
"""Return an iterator to get all asset caches"""
|
||||||
|
return (asset_cache for file_cache in self for asset_cache in file_cache)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def tmp_filepath(self):
|
||||||
|
return Path(bpy.app.tempdir) / self.filename
|
||||||
|
|
||||||
|
def read(self):
|
||||||
|
print(f"Read cache from {self.filepath}")
|
||||||
|
|
||||||
|
for file_cache_data in read_file(self.filepath):
|
||||||
|
self.add(file_cache_data)
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
|
def write(self, tmp=False):
|
||||||
|
filepath = self.filepath
|
||||||
|
if tmp:
|
||||||
|
filepath = self.tmp_filepath
|
||||||
|
|
||||||
|
print(f"Write cache file to {filepath}")
|
||||||
|
write_file(filepath, self._data)
|
||||||
|
return filepath
|
||||||
|
|
||||||
|
def add(self, file_cache_data=None):
|
||||||
|
file_cache = FileCache(self, file_cache_data)
|
||||||
|
|
||||||
|
self._data.append(file_cache)
|
||||||
|
|
||||||
|
return file_cache
|
||||||
|
|
||||||
|
def add_asset_cache(self, asset_cache_data, filepath=None):
|
||||||
|
if filepath is None:
|
||||||
|
filepath = asset_cache_data["filepath"]
|
||||||
|
|
||||||
|
file_cache = self.get(filepath)
|
||||||
|
if not file_cache:
|
||||||
|
file_cache = self.add()
|
||||||
|
|
||||||
|
file_cache.assets.add(asset_cache_data)
|
||||||
|
|
||||||
|
# def unflatten_cache(self, cache):
|
||||||
|
# """ Return a new unflattten list of asset data
|
||||||
|
# grouped by filepath"""
|
||||||
|
|
||||||
|
# new_cache = []
|
||||||
|
|
||||||
|
# cache = deepcopy(cache)
|
||||||
|
|
||||||
|
# cache.sort(key=lambda x : x['filepath'])
|
||||||
|
# groups = groupby(cache, key=lambda x : x['filepath'])
|
||||||
|
|
||||||
|
# keys = ['filepath', 'modified', 'library_id']
|
||||||
|
|
||||||
|
# for _, asset_datas in groups:
|
||||||
|
# asset_datas = list(asset_datas)
|
||||||
|
|
||||||
|
# #print(asset_datas[0])
|
||||||
|
|
||||||
|
# asset_info = {k:asset_datas[0][k] for k in keys}
|
||||||
|
# asset_info['assets'] = [{k:v for k, v in a.items() if k not in keys+['operation']} for a in asset_datas]
|
||||||
|
|
||||||
|
# new_cache.append(asset_info)
|
||||||
|
|
||||||
|
# return new_cache
|
||||||
|
|
||||||
|
def diff(self, new_cache=None):
|
||||||
|
"""Compare the library cache with it current state and return the cache differential"""
|
||||||
|
|
||||||
|
old_cache = self.read()
|
||||||
|
|
||||||
|
if new_cache is None:
|
||||||
|
new_cache = self
|
||||||
|
|
||||||
|
return LibraryCacheDiff(old_cache, new_cache)
|
||||||
|
|
||||||
|
def update(self, cache_diff):
|
||||||
|
# Update the cache with the operations
|
||||||
|
for asset_cache_diff in cache_diff:
|
||||||
|
file_cache = self.get(asset_cache_diff.filepath)
|
||||||
|
if not asset_cache:
|
||||||
|
print(f"Filepath {asset_cache_diff.filepath} not in {self}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
asset_cache = file_cache.get(asset_cache_diff.name)
|
||||||
|
|
||||||
|
if not asset_cache:
|
||||||
|
print(f"Asset {asset_cache_diff.name} not in file_cache {file_cache}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
if asset_cache_diff.operation == "REMOVE":
|
||||||
|
file_cache.assets.remove(asset_cache_diff.name)
|
||||||
|
|
||||||
|
elif asset_cache_diff.operation in ("MODIFY", "ADD"):
|
||||||
|
asset_cache.set_data(asset_cache_diff.asset_cache.to_dict())
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
return len(self._data)
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
return iter(self._data)
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
if isinstance(key, str):
|
||||||
|
return self.to_dict()[key]
|
||||||
|
else:
|
||||||
|
return self._data[key]
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
return {a.filepath: a for a in self}
|
||||||
|
|
||||||
|
def get(self, filepath):
|
||||||
|
return next((a for a in self if a.filepath == filepath), None)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"LibraryCache(library_id={self.library_id})"
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import argparse
|
||||||
|
import fnmatch
|
||||||
|
import importlib.util
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# import module utils without excuting __init__
|
||||||
|
spec = importlib.util.spec_from_file_location(
|
||||||
|
"utils", Path(__file__).parent / "file_utils.py"
|
||||||
|
)
|
||||||
|
utils = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(utils)
|
||||||
|
|
||||||
|
|
||||||
|
def synchronize(src, dst, only_new=False, only_recent=False):
|
||||||
|
|
||||||
|
excludes = ["*.sync-conflict-*", ".*"]
|
||||||
|
includes = ["*.blend", "blender_assets.cats.txt"]
|
||||||
|
|
||||||
|
utils.copy_dir(
|
||||||
|
src,
|
||||||
|
dst,
|
||||||
|
only_new=only_new,
|
||||||
|
only_recent=only_recent,
|
||||||
|
excludes=excludes,
|
||||||
|
includes=includes,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Add Comment To the tracker",
|
||||||
|
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument("--src")
|
||||||
|
parser.add_argument("--dst")
|
||||||
|
parser.add_argument("--only-new", type=json.loads, default="false")
|
||||||
|
parser.add_argument("--only-recent", type=json.loads, default="false")
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
synchronize(**vars(args))
|
||||||
@@ -9,51 +9,52 @@ import string
|
|||||||
class TemplateFormatter(string.Formatter):
|
class TemplateFormatter(string.Formatter):
|
||||||
def format_field(self, value, format_spec):
|
def format_field(self, value, format_spec):
|
||||||
if isinstance(value, str):
|
if isinstance(value, str):
|
||||||
spec, sep = [*format_spec.split(':'), None][:2]
|
spec, sep = [*format_spec.split(":"), None][:2]
|
||||||
|
|
||||||
if sep:
|
if sep:
|
||||||
value = value.replace('_', ' ')
|
value = value.replace("_", " ")
|
||||||
value = value = re.sub(r'([a-z])([A-Z])', rf'\1{sep}\2', value)
|
value = value = re.sub(r"([a-z])([A-Z])", rf"\1{sep}\2", value)
|
||||||
value = value.replace(' ', sep)
|
value = value.replace(" ", sep)
|
||||||
|
|
||||||
if spec == 'u':
|
if spec == "u":
|
||||||
value = value.upper()
|
value = value.upper()
|
||||||
elif spec == 'l':
|
elif spec == "l":
|
||||||
value = value.lower()
|
value = value.lower()
|
||||||
elif spec == 't':
|
elif spec == "t":
|
||||||
value = value.title()
|
value = value.title()
|
||||||
|
|
||||||
return super().format(value, format_spec)
|
return super().format(value, format_spec)
|
||||||
|
|
||||||
|
|
||||||
class Template:
|
class Template:
|
||||||
field_pattern = re.compile(r'{(\w+)\*{0,2}}')
|
field_pattern = re.compile(r"{(\w+)\*{0,2}}")
|
||||||
field_pattern_recursive = re.compile(r'{(\w+)\*{2}}')
|
field_pattern_recursive = re.compile(r"{(\w+)\*{2}}")
|
||||||
|
|
||||||
def __init__(self, template):
|
def __init__(self, template):
|
||||||
#asset_data_path = Path(lib_path) / ASSETLIB_FILENAME
|
# asset_data_path = Path(lib_path) / ASSETLIB_FILENAME
|
||||||
|
|
||||||
self.raw = template
|
self.raw = template
|
||||||
self.formatter = TemplateFormatter()
|
self.formatter = TemplateFormatter()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def glob_pattern(self):
|
def glob_pattern(self):
|
||||||
pattern = self.field_pattern_recursive.sub('**', self.raw)
|
pattern = self.field_pattern_recursive.sub("**", self.raw)
|
||||||
pattern = self.field_pattern.sub('*', pattern)
|
pattern = self.field_pattern.sub("*", pattern)
|
||||||
return pattern
|
return pattern
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def re_pattern(self):
|
def re_pattern(self):
|
||||||
pattern = self.field_pattern_recursive.sub('([\\\w -_.\/]+)', self.raw)
|
pattern = self.field_pattern_recursive.sub("([\\\w -_.\/]+)", self.raw)
|
||||||
pattern = self.field_pattern.sub('([\\\w -_.]+)', pattern)
|
pattern = self.field_pattern.sub("([\\\w -_.]+)", pattern)
|
||||||
pattern = pattern.replace('?', '.')
|
pattern = pattern.replace("?", ".")
|
||||||
pattern = pattern.replace('*', '.*')
|
pattern = pattern.replace("*", ".*")
|
||||||
|
|
||||||
return re.compile(pattern)
|
return re.compile(pattern)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def fields(self):
|
def fields(self):
|
||||||
return self.field_pattern.findall(self.raw)
|
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):
|
def parse(self, path):
|
||||||
|
|
||||||
@@ -61,7 +62,7 @@ class Template:
|
|||||||
|
|
||||||
res = self.re_pattern.findall(path)
|
res = self.re_pattern.findall(path)
|
||||||
if not res:
|
if not res:
|
||||||
print('Could not parse {path} with {self.re_pattern}')
|
print("Could not parse {path} with {self.re_pattern}")
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
fields = self.fields
|
fields = self.fields
|
||||||
@@ -71,7 +72,7 @@ class Template:
|
|||||||
else:
|
else:
|
||||||
field_values = res[0]
|
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):
|
def norm_data(self, data):
|
||||||
norm_data = {}
|
norm_data = {}
|
||||||
@@ -89,17 +90,17 @@ class Template:
|
|||||||
data = {**(data or {}), **kargs}
|
data = {**(data or {}), **kargs}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
#print('FORMAT', self.raw, data)
|
# print('FORMAT', self.raw, data)
|
||||||
path = self.formatter.format(self.raw, **self.norm_data(data))
|
path = self.formatter.format(self.raw, **self.norm_data(data))
|
||||||
except KeyError as e:
|
except KeyError as e:
|
||||||
print(f'Cannot format {self.raw} with {data}, field {e} is missing')
|
print(f"Cannot format {self.raw} with {data}, field {e} is missing")
|
||||||
return
|
return
|
||||||
|
|
||||||
path = os.path.expandvars(path)
|
path = os.path.expandvars(path)
|
||||||
return Path(path)
|
return Path(path)
|
||||||
|
|
||||||
def glob(self, directory, pattern=None):
|
def glob(self, directory, pattern=None):
|
||||||
'''If pattern is given it need to be absolute'''
|
"""If pattern is given it need to be absolute"""
|
||||||
if pattern is None:
|
if pattern is None:
|
||||||
pattern = Path(directory, self.glob_pattern).as_posix()
|
pattern = Path(directory, self.glob_pattern).as_posix()
|
||||||
|
|
||||||
@@ -114,14 +115,14 @@ class Template:
|
|||||||
pattern = self.format(data, **kargs)
|
pattern = self.format(data, **kargs)
|
||||||
|
|
||||||
pattern_str = str(pattern)
|
pattern_str = str(pattern)
|
||||||
if '*' not in pattern_str and '?' not in pattern_str:
|
if "*" not in pattern_str and "?" not in pattern_str:
|
||||||
return pattern
|
return pattern
|
||||||
|
|
||||||
paths = glob(pattern.as_posix())
|
paths = glob(pattern.as_posix())
|
||||||
if paths:
|
if paths:
|
||||||
return Path(paths[0])
|
return Path(paths[0])
|
||||||
|
|
||||||
#return pattern
|
# return pattern
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f'Template({self.raw})'
|
return f"Template({self.raw})"
|
||||||
+13
-23
@@ -3,35 +3,25 @@ import bpy
|
|||||||
|
|
||||||
|
|
||||||
DATA_TYPE_ITEMS = [
|
DATA_TYPE_ITEMS = [
|
||||||
("NodeTree", "Node Group", "", "NODETREE", 0),
|
("ACTION", "Action", "", "ACTION", 0),
|
||||||
("Material", "Material", "", "MATERIAL", 1),
|
("COLLECTION", "Collection", "", "OUTLINER_OB_GROUP_INSTANCE", 1),
|
||||||
("Object", "Object", "", "OBJECT_DATA", 2),
|
("FILE", "File", "", "FILE", 2),
|
||||||
("Action", "Action", "", "ACTION", 3),
|
|
||||||
("Collection", "Collection", "", "OUTLINER_OB_GROUP_INSTANCE", 4),
|
|
||||||
("File", "File", "", "FILE", 5)
|
|
||||||
]
|
]
|
||||||
|
|
||||||
DATA_TYPE_GEO_ITEMS = [DATA_TYPE_ITEMS[0], DATA_TYPE_ITEMS[2]]
|
|
||||||
DATA_TYPE_SHADING_ITEMS = [DATA_TYPE_ITEMS[0], DATA_TYPE_ITEMS[1]]
|
|
||||||
|
|
||||||
CATALOG_ITEMS = {}
|
|
||||||
DATA_TYPES = [i[0] for i in DATA_TYPE_ITEMS]
|
DATA_TYPES = [i[0] for i in DATA_TYPE_ITEMS]
|
||||||
ICONS = {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"
|
ASSETLIB_FILENAME = "blender_assets.libs.json"
|
||||||
MODULE_DIR = Path(__file__).parent
|
MODULE_DIR = Path(__file__).parent
|
||||||
RESOURCES_DIR = MODULE_DIR / 'resources'
|
RESOURCES_DIR = MODULE_DIR / "resources"
|
||||||
|
|
||||||
PLUGINS_DIR = MODULE_DIR / 'plugins'
|
LIBRARY_TYPE_DIR = MODULE_DIR / "library_types"
|
||||||
PLUGINS = {}
|
LIBRARY_TYPES = []
|
||||||
PLUGINS_ITEMS = [('NONE', 'None', '', 0)]
|
|
||||||
|
|
||||||
LIB_DIR = MODULE_DIR / 'libs'
|
ADAPTER_DIR = MODULE_DIR / "adapters"
|
||||||
LIB_ITEMS = []
|
ADAPTERS = []
|
||||||
|
|
||||||
SCRIPTS_DIR = MODULE_DIR / 'scripts'
|
PREVIEW_ASSETS_SCRIPT = MODULE_DIR / "common" / "preview_assets.py"
|
||||||
|
|
||||||
PREVIEW_ASSETS_SCRIPT = MODULE_DIR / 'common' / 'preview_assets.py'
|
|
||||||
|
|
||||||
#ADD_ASSET_DICT = {}
|
|
||||||
|
|
||||||
|
# ADD_ASSET_DICT = {}
|
||||||
|
|||||||
@@ -1,319 +0,0 @@
|
|||||||
"""
|
|
||||||
Util function for this addon
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
from pathlib import Path
|
|
||||||
import inspect
|
|
||||||
from datetime import datetime
|
|
||||||
from time import perf_counter
|
|
||||||
import platform
|
|
||||||
|
|
||||||
import bpy
|
|
||||||
from .catalog import Catalog, read_catalog
|
|
||||||
from .bl_utils import get_addon_prefs, get_asset_type
|
|
||||||
from .file_utils import read_file, write_file, cache, import_module_from_path
|
|
||||||
|
|
||||||
|
|
||||||
def thumbnail_blend_file(input_blend, output_img):
|
|
||||||
input_blend = Path(input_blend).resolve()
|
|
||||||
output_img = Path(output_img).resolve()
|
|
||||||
|
|
||||||
print(f'Thumbnailing {input_blend} to {output_img}')
|
|
||||||
blender_thumbnailer = Path(bpy.app.binary_path).parent / 'blender-thumbnailer'
|
|
||||||
|
|
||||||
output_img.parent.mkdir(exist_ok=True, parents=True)
|
|
||||||
|
|
||||||
subprocess.call([blender_thumbnailer, str(input_blend), str(output_img)])
|
|
||||||
|
|
||||||
success = output_img.exists()
|
|
||||||
|
|
||||||
if not success:
|
|
||||||
empty_preview = RESOURCES_DIR / 'empty_preview.png'
|
|
||||||
shutil.copy(str(empty_preview), str(output_img))
|
|
||||||
|
|
||||||
return success
|
|
||||||
|
|
||||||
|
|
||||||
def get_active_library():
|
|
||||||
'''Get the pref library properties from the active library of the asset browser'''
|
|
||||||
prefs = get_addon_prefs()
|
|
||||||
lib_ref = bpy.context.space_data.params.asset_library_reference
|
|
||||||
|
|
||||||
#Check for merged library
|
|
||||||
for l in prefs.libraries:
|
|
||||||
if l.name == lib_ref:
|
|
||||||
return l
|
|
||||||
|
|
||||||
|
|
||||||
def update_library_path():
|
|
||||||
"""Removing all asset libraries and recreate them"""
|
|
||||||
|
|
||||||
print("update_library_path")
|
|
||||||
|
|
||||||
addon_prefs = get_addon_prefs()
|
|
||||||
libs = bpy.context.preferences.filepaths.asset_libraries
|
|
||||||
|
|
||||||
for i, lib in reversed(list(enumerate(libs))):
|
|
||||||
if (addon_lib := addon_prefs.libraries.get(lib.name)): # and addon_lib.path == lib.path
|
|
||||||
bpy.ops.preferences.asset_library_remove(index=i)
|
|
||||||
|
|
||||||
for addon_lib in addon_prefs.libraries:
|
|
||||||
if not addon_lib.use:
|
|
||||||
continue
|
|
||||||
bpy.ops.preferences.asset_library_add(directory=str(addon_lib.path))
|
|
||||||
libs[-1].name = addon_lib.name
|
|
||||||
|
|
||||||
|
|
||||||
def load_library_config(config_path):
|
|
||||||
""""Load library prefs from config path"""
|
|
||||||
if not config_path:
|
|
||||||
return []
|
|
||||||
|
|
||||||
config_path = Path(config_path)
|
|
||||||
|
|
||||||
if not config_path.exists():
|
|
||||||
print(f'Config {config_path} not exist')
|
|
||||||
return []
|
|
||||||
|
|
||||||
prefs = get_addon_prefs()
|
|
||||||
|
|
||||||
libs = []
|
|
||||||
for lib_dict in read_file(config_path):
|
|
||||||
lib = prefs.libraries.add()
|
|
||||||
lib.is_user = False
|
|
||||||
lib.set_dict(lib_dict)
|
|
||||||
libs.append(lib)
|
|
||||||
|
|
||||||
return libs
|
|
||||||
|
|
||||||
|
|
||||||
def load_libraries():
|
|
||||||
""""Load library prefs from config pref and env"""
|
|
||||||
prefs = get_addon_prefs()
|
|
||||||
|
|
||||||
bl_libs = bpy.context.preferences.filepaths.asset_libraries
|
|
||||||
for i, bl_lib in reversed(list(enumerate(bl_libs))):
|
|
||||||
addon_lib = prefs.libraries.get(bl_lib.name)
|
|
||||||
if not addon_lib or addon_lib.is_user:
|
|
||||||
continue
|
|
||||||
|
|
||||||
bpy.ops.preferences.asset_library_remove(index=i)
|
|
||||||
|
|
||||||
# Remove lib from addons preferences
|
|
||||||
for i, addon_lib in reversed(list(enumerate(prefs.libraries))):
|
|
||||||
if not addon_lib.is_user:
|
|
||||||
prefs.libraries.remove(i)
|
|
||||||
|
|
||||||
env_config = os.getenv('ASSET_LIBRARY_CONFIG')
|
|
||||||
libs = load_library_config(env_config) + load_library_config(prefs.config_path)
|
|
||||||
|
|
||||||
return libs
|
|
||||||
|
|
||||||
|
|
||||||
def clear_time_tag(asset):
|
|
||||||
# Created time tag
|
|
||||||
for tag in list(asset.asset_data.tags):
|
|
||||||
try:
|
|
||||||
datetime.strptime(tag.name, "%Y-%m-%d %H:%M")
|
|
||||||
asset.asset_data.tags.remove(tag)
|
|
||||||
except ValueError:
|
|
||||||
continue
|
|
||||||
|
|
||||||
|
|
||||||
def create_time_tag(asset):
|
|
||||||
asset.asset_data.tags.new(datetime.now().strftime("%Y-%m-%d %H:%M"))
|
|
||||||
|
|
||||||
|
|
||||||
def version_file(path, save_versions=3):
|
|
||||||
if path.exists():
|
|
||||||
for i in range(save_versions):
|
|
||||||
version = save_versions - i
|
|
||||||
version_path = path.with_suffix(f'.blend{version}')
|
|
||||||
if not version_path.exists():
|
|
||||||
continue
|
|
||||||
if i == 0:
|
|
||||||
version_path.unlink()
|
|
||||||
else:
|
|
||||||
version_path.rename(path.with_suffix(f'.blend{version+1}'))
|
|
||||||
|
|
||||||
path.rename(path.with_suffix(f'.blend1'))
|
|
||||||
|
|
||||||
|
|
||||||
def list_datablocks(blend_file, asset_types={"objects", "materials", "node_groups"}):
|
|
||||||
blend_data = {}
|
|
||||||
with bpy.data.temp_data(filepath=str(blend_file)) as temp_data:
|
|
||||||
with temp_data.libraries.load(str(blend_file), link=True) as (data_from, data_to):
|
|
||||||
for asset_type in asset_types:
|
|
||||||
blend_data[asset_type] = getattr(data_from, asset_type)
|
|
||||||
|
|
||||||
return blend_data
|
|
||||||
|
|
||||||
|
|
||||||
def get_asset_data(blend_file, asset_type, name, preview=False):
|
|
||||||
with bpy.data.temp_data(filepath=str(blend_file)) as temp_data:
|
|
||||||
with temp_data.libraries.load(str(blend_file), link=True) as (data_from, data_to):
|
|
||||||
if name not in getattr(data_from, asset_type):
|
|
||||||
return
|
|
||||||
setattr(data_to, asset_type, [name])
|
|
||||||
|
|
||||||
if assets := getattr(data_to, asset_type):
|
|
||||||
asset = assets[0]
|
|
||||||
asset_data = asset.asset_data
|
|
||||||
|
|
||||||
data = {
|
|
||||||
"description": asset_data.description,
|
|
||||||
"catalog_id": asset_data.catalog_id,
|
|
||||||
"catalog_simple_name": asset_data.catalog_simple_name,
|
|
||||||
"path": blend_file,
|
|
||||||
"tags": list(asset_data.tags.keys())
|
|
||||||
}
|
|
||||||
|
|
||||||
if asset.preview and preview:
|
|
||||||
image_size = asset.preview.image_size
|
|
||||||
preview_pixels = [0] * image_size[0] * image_size[1] * 4
|
|
||||||
asset.preview.image_pixels_float.foreach_get(preview_pixels)
|
|
||||||
|
|
||||||
data["preview_pixels"] = preview_pixels
|
|
||||||
data["preview_size"] = list(image_size)
|
|
||||||
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
def find_asset_data(name, asset_type, preview=False):
|
|
||||||
"""Find info about an asset found in library"""
|
|
||||||
|
|
||||||
bl_libs = bpy.context.preferences.filepaths.asset_libraries
|
|
||||||
|
|
||||||
# First search for a blend with the same name
|
|
||||||
for bl_lib in bl_libs:
|
|
||||||
for blend_file in Path(bl_lib.path).glob(f"**/{name}.blend"):
|
|
||||||
if asset_data := get_asset_data(blend_file, asset_type, name, preview=preview):
|
|
||||||
asset_data['library'] = bl_lib
|
|
||||||
return asset_data
|
|
||||||
|
|
||||||
# for bl_lib in bl_libs:
|
|
||||||
# for blend_file in Path(bl_lib.path).glob("**/*.blend"):
|
|
||||||
# if asset_data := get_asset_data(blend_file, asset_type, name):
|
|
||||||
# return bl_lib, asset_data
|
|
||||||
|
|
||||||
|
|
||||||
def get_filepath_library(filepath):
|
|
||||||
for lib in bpy.context.preferences.filepaths.asset_libraries:
|
|
||||||
if bpy.path.is_subdir(filepath, lib.path):
|
|
||||||
return lib
|
|
||||||
|
|
||||||
|
|
||||||
def get_asset_catalog_path(asset, fallback=''):
|
|
||||||
if asset.local_id:
|
|
||||||
path = Path(bpy.data.filepath).parent
|
|
||||||
|
|
||||||
elif (lib := get_filepath_library(asset.full_library_path)):
|
|
||||||
if lib:
|
|
||||||
path = lib.path
|
|
||||||
else:
|
|
||||||
return fallback
|
|
||||||
|
|
||||||
if not (catalog := read_catalog(path)):
|
|
||||||
return fallback
|
|
||||||
|
|
||||||
if not (catalog_item := catalog.get(id=asset.metadata.catalog_id, fallback=fallback)):
|
|
||||||
return fallback
|
|
||||||
|
|
||||||
return catalog_item.path
|
|
||||||
|
|
||||||
|
|
||||||
def get_asset_full_path(asset):
|
|
||||||
"""Get a path that represent all informations about an asset path/type/catalog/name"""
|
|
||||||
|
|
||||||
asset_path = asset.full_library_path
|
|
||||||
if asset.local_id:
|
|
||||||
asset_path = f'{bpy.data.filepath}/{asset_path}'
|
|
||||||
|
|
||||||
asset_type, asset_name = Path(asset.full_path).parts[-2:]
|
|
||||||
asset_type = get_asset_type(asset_type)
|
|
||||||
|
|
||||||
return Path(asset_path, asset_type, asset_name).as_posix()
|
|
||||||
|
|
||||||
|
|
||||||
def get_asset_source(datablock):
|
|
||||||
weak_reference = datablock.library_weak_reference
|
|
||||||
if isinstance(datablock, bpy.types.Object) and datablock.data:
|
|
||||||
weak_reference = datablock.data.library_weak_reference
|
|
||||||
|
|
||||||
if weak_reference and (source_path := Path(weak_reference.filepath)).exists():
|
|
||||||
return source_path
|
|
||||||
|
|
||||||
asset_libraries = context.preferences.filepaths.asset_libraries
|
|
||||||
for asset_library in asset_libraries:
|
|
||||||
library_path = Path(asset_library.path)
|
|
||||||
if blend_files := list(library_path.glob(f"**/{datablock.name}.blend")):
|
|
||||||
return
|
|
||||||
|
|
||||||
return datablock.library_weak_reference
|
|
||||||
|
|
||||||
|
|
||||||
def get_blender_cache_dir():
|
|
||||||
if platform.system() == 'Linux':
|
|
||||||
cache_folder = os.path.expandvars('$HOME/.cache/blender')
|
|
||||||
elif platform.system() == 'Windows':
|
|
||||||
cache_folder = os.path.expanduser('%USERPROFILE%/AppData/Local/Blender Foundation/Blender')
|
|
||||||
elif platform.system() == 'Darwin':
|
|
||||||
cache_folder = '/Library/Caches/Blender'
|
|
||||||
|
|
||||||
return Path(cache_folder)
|
|
||||||
|
|
||||||
|
|
||||||
def find_asset_source(library_map, asset_type, name):
|
|
||||||
return next( (l for l, blend_data in sorted(library_map.items(), key=lambda x: x[1]['st_mtime'], reverse=True)
|
|
||||||
if name in blend_data['node_groups']), None)
|
|
||||||
|
|
||||||
|
|
||||||
def asset_library_map():
|
|
||||||
""""Get a mapping of all datablocks of the blend files from the libraries"""
|
|
||||||
asset_libraries = bpy.context.preferences.filepaths.asset_libraries
|
|
||||||
|
|
||||||
cache_file = get_blender_cache_dir() / 'asset-library.json'
|
|
||||||
cache = None
|
|
||||||
if cache_file.exists():
|
|
||||||
cache = read_file(cache_file)
|
|
||||||
|
|
||||||
if cache is None:
|
|
||||||
cache = {}
|
|
||||||
|
|
||||||
file_keys = []
|
|
||||||
|
|
||||||
for asset_library in asset_libraries:
|
|
||||||
library_path = Path(asset_library.path)
|
|
||||||
|
|
||||||
for bl_file in library_path.glob("**/*.blend"):
|
|
||||||
file_keys.append(file_key := bl_file.as_posix())
|
|
||||||
|
|
||||||
st_mtime = bl_file.stat().st_mtime
|
|
||||||
if (bl_cache := cache.get(file_key)) and bl_cache.get('st_mtime') >= st_mtime:
|
|
||||||
continue
|
|
||||||
|
|
||||||
datablocks = list_datablocks(bl_file)
|
|
||||||
cache[file_key] = dict(st_mtime=st_mtime, **datablocks)
|
|
||||||
|
|
||||||
# Remove map when the blend not exists anymore
|
|
||||||
for file_key in list(cache.keys()):
|
|
||||||
if file_key not in file_keys:
|
|
||||||
del cache[file_key]
|
|
||||||
|
|
||||||
write_file(cache_file, cache)
|
|
||||||
|
|
||||||
return cache
|
|
||||||
|
|
||||||
# print(perf_counter() - t0)
|
|
||||||
|
|
||||||
# t0 = perf_counter()
|
|
||||||
# for asset_library in asset_libraries:
|
|
||||||
# library_path = Path(asset_library.path)
|
|
||||||
|
|
||||||
# for blend_file in library_path.glob("**/*.blend"):
|
|
||||||
# with bpy.data.libraries.load(str(blend_file), link=True) as (data_from, data_to):
|
|
||||||
# node_groups = data_from.node_groups
|
|
||||||
# print(node_groups)
|
|
||||||
|
|
||||||
# print(perf_counter() - t0)
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
|
|
||||||
from . node import ui as node_ui
|
|
||||||
from . node import operator as node_operator
|
|
||||||
from . material import ui as material_ui
|
|
||||||
from . material import operator as material_operator
|
|
||||||
|
|
||||||
bl_modules = (
|
|
||||||
node_ui,
|
|
||||||
node_operator,
|
|
||||||
material_ui,
|
|
||||||
material_operator
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def register():
|
|
||||||
"""Register the addon Asset Library for Blender"""
|
|
||||||
|
|
||||||
for mod in bl_modules:
|
|
||||||
mod.register()
|
|
||||||
|
|
||||||
|
|
||||||
def unregister():
|
|
||||||
"""Unregister the addon Asset Library for Blender"""
|
|
||||||
|
|
||||||
for mod in reversed(bl_modules):
|
|
||||||
mod.unregister()
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
|
|
||||||
from asset_library.data_type.action import (
|
|
||||||
keymaps,
|
|
||||||
operators,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
import bpy
|
|
||||||
|
|
||||||
def register():
|
|
||||||
operators.register()
|
|
||||||
keymaps.register()
|
|
||||||
|
|
||||||
def unregister():
|
|
||||||
operators.unregister()
|
|
||||||
keymaps.unregister()
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
|
|
||||||
import argparse
|
|
||||||
import bpy
|
|
||||||
import json
|
|
||||||
import sys
|
|
||||||
|
|
||||||
from pathlib import Path
|
|
||||||
#sys.path.append(str(Path(__file__).parents[3]))
|
|
||||||
from asset_library.common.bl_utils import (
|
|
||||||
get_preview,
|
|
||||||
)
|
|
||||||
|
|
||||||
def clear_asset(action_name='', use_fake_user=False):
|
|
||||||
|
|
||||||
scn = bpy.context.scene
|
|
||||||
|
|
||||||
action = bpy.data.actions.get(action_name)
|
|
||||||
if not action:
|
|
||||||
print(f'No {action_name} not found.')
|
|
||||||
bpy.ops.wm.quit_blender()
|
|
||||||
|
|
||||||
action.asset_clear()
|
|
||||||
if use_fake_user:
|
|
||||||
action.use_fake_user = True
|
|
||||||
else:
|
|
||||||
preview = get_preview(asset_path=bpy.data.filepath, asset_name=action_name)
|
|
||||||
if preview:
|
|
||||||
preview.unlink()
|
|
||||||
bpy.data.actions.remove(action)
|
|
||||||
|
|
||||||
bpy.ops.wm.save_mainfile(
|
|
||||||
filepath=bpy.data.filepath, compress=True, exit=True
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__' :
|
|
||||||
parser = argparse.ArgumentParser(description='Add Comment To the tracker',
|
|
||||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
|
||||||
|
|
||||||
parser.add_argument('--action-name')
|
|
||||||
parser.add_argument('--use-fake-user', type=json.loads, default='false')
|
|
||||||
|
|
||||||
if '--' in sys.argv :
|
|
||||||
index = sys.argv.index('--')
|
|
||||||
sys.argv = [sys.argv[index-1], *sys.argv[index+1:]]
|
|
||||||
|
|
||||||
args = parser.parse_args()
|
|
||||||
clear_asset(**vars(args))
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
|
|
||||||
import bpy
|
|
||||||
|
|
||||||
|
|
||||||
def draw_context_menu(layout):
|
|
||||||
params = bpy.context.space_data.params
|
|
||||||
asset = bpy.context.asset_file_handle
|
|
||||||
|
|
||||||
layout.operator("assetlib.open_blend", text="Open blend file")#.asset = asset.name
|
|
||||||
layout.operator("assetlib.play_preview", text="Play Preview")
|
|
||||||
|
|
||||||
layout.separator()
|
|
||||||
|
|
||||||
layout.operator_context = 'INVOKE_DEFAULT'
|
|
||||||
|
|
||||||
#layout.operator("assetlib.rename_asset", text="Rename Action")
|
|
||||||
layout.operator("assetlib.remove_assets", text="Remove Assets")
|
|
||||||
layout.operator("assetlib.edit_data", text="Edit Asset data")
|
|
||||||
|
|
||||||
#layout.operator("actionlib.clear_asset", text="Clear Asset (Fake User)").use_fake_user = True
|
|
||||||
|
|
||||||
layout.separator()
|
|
||||||
|
|
||||||
layout.operator("actionlib.apply_selected_action", text="Apply Pose").flipped = False
|
|
||||||
layout.operator("actionlib.apply_selected_action", text="Apply Pose (Flipped)").flipped = True
|
|
||||||
|
|
||||||
layout.separator()
|
|
||||||
|
|
||||||
layout.operator("poselib.blend_pose_asset_for_keymap", text="Blend Pose").flipped = False
|
|
||||||
layout.operator("poselib.blend_pose_asset_for_keymap", text="Blend Pose (Flipped)").flipped = True
|
|
||||||
|
|
||||||
layout.separator()
|
|
||||||
|
|
||||||
layout.operator("poselib.pose_asset_select_bones", text="Select Bones").selected_side = 'CURRENT'
|
|
||||||
layout.operator("poselib.pose_asset_select_bones", text="Select Bones (Flipped)").selected_side = 'FLIPPED'
|
|
||||||
layout.operator("poselib.pose_asset_select_bones", text="Select Bones (Both)").selected_side = 'BOTH'
|
|
||||||
|
|
||||||
layout.separator()
|
|
||||||
#layout.operator("asset.library_refresh")
|
|
||||||
if params.display_type == 'THUMBNAIL':
|
|
||||||
layout.prop_menu_enum(params, "display_size")
|
|
||||||
|
|
||||||
|
|
||||||
def draw_header(layout):
|
|
||||||
'''Draw the header of the Asset Browser Window'''
|
|
||||||
|
|
||||||
layout.separator()
|
|
||||||
layout.operator("actionlib.store_anim_pose", text='Add Action', icon='FILE_NEW')
|
|
||||||
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
|
|
||||||
import argparse
|
|
||||||
import bpy
|
|
||||||
import json
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
|
|
||||||
from pathlib import Path
|
|
||||||
#sys.path.append(str(Path(__file__).parents[3]))
|
|
||||||
from asset_library.common.bl_utils import (
|
|
||||||
get_preview,
|
|
||||||
)
|
|
||||||
|
|
||||||
def rename_pose(src_name='', dst_name=''):
|
|
||||||
|
|
||||||
scn = bpy.context.scene
|
|
||||||
action = bpy.data.actions.get(src_name)
|
|
||||||
if not action:
|
|
||||||
print(f'No {src_name} not found.')
|
|
||||||
bpy.ops.wm.quit_blender()
|
|
||||||
|
|
||||||
action.name = dst_name
|
|
||||||
preview = get_preview(asset_path=bpy.data.filepath, asset_name=src_name)
|
|
||||||
if preview:
|
|
||||||
preview.rename(re.sub(src_name, dst_name, str(preview)))
|
|
||||||
|
|
||||||
bpy.ops.wm.save_mainfile(
|
|
||||||
filepath=bpy.data.filepath, compress=True, exit=True
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__' :
|
|
||||||
parser = argparse.ArgumentParser(description='Add Comment To the tracker',
|
|
||||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
|
||||||
|
|
||||||
parser.add_argument('--src-name')
|
|
||||||
parser.add_argument('--dst-name')
|
|
||||||
|
|
||||||
if '--' in sys.argv :
|
|
||||||
index = sys.argv.index('--')
|
|
||||||
sys.argv = [sys.argv[index-1], *sys.argv[index+1:]]
|
|
||||||
|
|
||||||
args = parser.parse_args()
|
|
||||||
rename_pose(**vars(args))
|
|
||||||
@@ -1,170 +0,0 @@
|
|||||||
|
|
||||||
import argparse
|
|
||||||
import sys
|
|
||||||
import json
|
|
||||||
from pathlib import Path
|
|
||||||
import bpy
|
|
||||||
import re
|
|
||||||
import uuid
|
|
||||||
from itertools import groupby
|
|
||||||
|
|
||||||
from asset_library.constants import ASSETLIB_FILENAME, MODULE_DIR
|
|
||||||
from asset_library.common.bl_utils import thumbnail_blend_file
|
|
||||||
from asset_library.common.functions import command
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@command
|
|
||||||
def bundle_library(source_directory, bundle_directory, template_info, thumbnail_template,
|
|
||||||
template=None, data_file=None):
|
|
||||||
|
|
||||||
field_pattern = r'{(\w+)}'
|
|
||||||
asset_data_path = Path(bundle_directory) / ASSETLIB_FILENAME
|
|
||||||
|
|
||||||
glob_pattern = re.sub(field_pattern, '*', template)
|
|
||||||
re_pattern = re.sub(field_pattern, r'([\\w -_.]+)', template)
|
|
||||||
re_pattern = re_pattern.replace('?', '.')
|
|
||||||
|
|
||||||
field_names = re.findall(field_pattern, template)
|
|
||||||
|
|
||||||
asset_file_datas = []
|
|
||||||
for f in sorted(Path(source_directory).glob(glob_pattern)):
|
|
||||||
rel_path = f.relative_to(source_directory).as_posix()
|
|
||||||
|
|
||||||
field_values = re.findall(re_pattern, rel_path)[0]
|
|
||||||
field_data = {k:v for k,v in zip(field_names, field_values)}
|
|
||||||
|
|
||||||
name = field_data.get('name', f.stem)
|
|
||||||
thumbnail = (f / thumbnail_template.format(name=name)).resolve()
|
|
||||||
asset_data = (f / template_info.format(name=name)).resolve()
|
|
||||||
|
|
||||||
catalogs = sorted([v for k,v in sorted(field_data.items()) if re.findall('cat[0-9]+', k)])
|
|
||||||
catalogs = [c.replace('_', ' ').title() for c in catalogs]
|
|
||||||
|
|
||||||
if not thumbnail.exists():
|
|
||||||
thumbnail_blend_file(f, thumbnail)
|
|
||||||
|
|
||||||
asset_data = {
|
|
||||||
'catalog' : '/'.join(catalogs),
|
|
||||||
'preview' : thumbnail.as_posix(), #'./' + bpy.path.relpath(str(thumbnail), start=str(f))[2:],
|
|
||||||
'filepath' : f.as_posix(), #'./' + bpy.path.relpath(str(f), start=str(asset_data_path))[2:],
|
|
||||||
'name': name,
|
|
||||||
'tags': [],
|
|
||||||
'metadata': {'filepath': f.as_posix()}
|
|
||||||
}
|
|
||||||
|
|
||||||
asset_file_datas.append(asset_data)
|
|
||||||
|
|
||||||
# Write json data file to store all asset found
|
|
||||||
print(f'Writing asset data file to, {asset_data_path}')
|
|
||||||
asset_data_path.write_text(json.dumps(asset_file_datas, indent=4))
|
|
||||||
|
|
||||||
#script = MODULE_DIR / 'common' / 'bundle_blend.py'
|
|
||||||
#cmd = [bpy.app.binary_path, '--python', str(script), '--', '--filepath', str(filepath)]
|
|
||||||
#print(cmd)
|
|
||||||
#subprocess.call(cmd)
|
|
||||||
|
|
||||||
@command
|
|
||||||
def bundle_blend(filepath, depth=0):
|
|
||||||
#print('Bundle Blend...')
|
|
||||||
filepath = Path(filepath)
|
|
||||||
|
|
||||||
#asset_data_path = get_asset_datas_file(filepath)
|
|
||||||
|
|
||||||
asset_data_path = filepath / ASSETLIB_FILENAME
|
|
||||||
blend_name = filepath.name.replace(' ', '_').lower()
|
|
||||||
blend_path = (filepath / blend_name).with_suffix('.blend')
|
|
||||||
|
|
||||||
if not asset_data_path.exists():
|
|
||||||
raise Exception(f'The file {asset_data_path} not exist')
|
|
||||||
|
|
||||||
catalog_path = get_catalog_path(filepath)
|
|
||||||
catalog_data = read_catalog(catalog_path)
|
|
||||||
|
|
||||||
asset_file_data = json.loads(asset_data_path.read_text())
|
|
||||||
#asset_file_data = {i['catalog']:i for i in asset_file_data}
|
|
||||||
|
|
||||||
if depth == 0:
|
|
||||||
groups = [asset_file_data]
|
|
||||||
else:
|
|
||||||
asset_file_data.sort(key=lambda x :x['catalog'].split('/')[:depth])
|
|
||||||
groups = groupby(asset_file_data, key=lambda x :x['catalog'].split('/')[:depth])
|
|
||||||
|
|
||||||
#progress = 0
|
|
||||||
total_assets = len(asset_file_data)
|
|
||||||
|
|
||||||
i = 0
|
|
||||||
for sub_path, asset_datas in groups:
|
|
||||||
bpy.ops.wm.read_homefile(use_empty=True)
|
|
||||||
|
|
||||||
for asset_data in asset_datas:
|
|
||||||
blend_name = sub_path[-1].replace(' ', '_').lower()
|
|
||||||
blend_path = Path(filepath, *sub_path, blend_name).with_suffix('.blend')
|
|
||||||
|
|
||||||
if i % int(total_assets / 100) == 0:
|
|
||||||
print(f'Progress: {int(i / total_assets * 100)}')
|
|
||||||
|
|
||||||
col = bpy.data.collections.new(name=asset_data['name'])
|
|
||||||
|
|
||||||
# Seems slow
|
|
||||||
#bpy.context.scene.collection.children.link(col)
|
|
||||||
col.asset_mark()
|
|
||||||
|
|
||||||
with bpy.context.temp_override(id=col):
|
|
||||||
bpy.ops.ed.lib_id_load_custom_preview(
|
|
||||||
filepath=asset_data['preview']
|
|
||||||
)
|
|
||||||
|
|
||||||
col.asset_data.description = asset_data.get('description', '')
|
|
||||||
|
|
||||||
catalog_name = asset_data['catalog']
|
|
||||||
catalog = catalog_data.get(catalog_name)
|
|
||||||
if not catalog:
|
|
||||||
catalog = {'id': str(uuid.uuid4()), 'name': catalog_name}
|
|
||||||
catalog_data[catalog_name] = catalog
|
|
||||||
|
|
||||||
col.asset_data.catalog_id = catalog['id']
|
|
||||||
|
|
||||||
for k, v in asset_data.get('metadata', {}).items():
|
|
||||||
col.asset_data[k] = v
|
|
||||||
|
|
||||||
i += 1
|
|
||||||
|
|
||||||
print(f'Saving Blend to {blend_path}')
|
|
||||||
|
|
||||||
blend_path.mkdir(exist_ok=True, parents=True)
|
|
||||||
bpy.ops.wm.save_as_mainfile(filepath=str(blend_path), compress=True)
|
|
||||||
|
|
||||||
write_catalog(catalog_path, catalog_data)
|
|
||||||
|
|
||||||
bpy.ops.wm.quit_blender()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__' :
|
|
||||||
parser = argparse.ArgumentParser(description='bundle_blend',
|
|
||||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
|
||||||
|
|
||||||
parser.add_argument('--source-path')
|
|
||||||
parser.add_argument('--bundle-path')
|
|
||||||
parser.add_argument('--asset-data-template')
|
|
||||||
parser.add_argument('--thumbnail-template')
|
|
||||||
parser.add_argument('--template', default=None)
|
|
||||||
parser.add_argument('--data-file', default=None)
|
|
||||||
parser.add_argument('--depth', default=0, type=int)
|
|
||||||
|
|
||||||
if '--' in sys.argv :
|
|
||||||
index = sys.argv.index('--')
|
|
||||||
sys.argv = [sys.argv[index-1], *sys.argv[index+1:]]
|
|
||||||
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
bundle_library(
|
|
||||||
source_directory=args.source_directory,
|
|
||||||
bundle_directory=args.bundle_directory,
|
|
||||||
template_info=args.template_info,
|
|
||||||
thumbnail_template=args.thumbnail_template,
|
|
||||||
template=args.template,
|
|
||||||
data_file=args.data_file)
|
|
||||||
|
|
||||||
bundle_blend(filepath=args.bundle_directory, depth=args.depth)
|
|
||||||
@@ -1,107 +0,0 @@
|
|||||||
|
|
||||||
from pathlib import Path
|
|
||||||
from datetime import datetime
|
|
||||||
import subprocess
|
|
||||||
|
|
||||||
from ...core.bl_utils import get_bl_cmd
|
|
||||||
from ...core.lib_utils import get_asset_data
|
|
||||||
from ...core.catalog import read_catalog
|
|
||||||
from ... import constants
|
|
||||||
|
|
||||||
import bpy
|
|
||||||
from bpy.types import Operator
|
|
||||||
from bpy.props import StringProperty, EnumProperty
|
|
||||||
|
|
||||||
|
|
||||||
class ASSETLIB_OT_update_materials(Operator):
|
|
||||||
bl_idname = 'assetlibrary.update_materials'
|
|
||||||
bl_label = 'Update node'
|
|
||||||
bl_options = {"REGISTER", "UNDO"}
|
|
||||||
|
|
||||||
selection : EnumProperty(items=[(s, s.title(), '') for s in ('ALL', 'OBJECT', 'CURRENT')], default="CURRENT")
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def poll(cls, context):
|
|
||||||
return context.object and context.object.active_material
|
|
||||||
|
|
||||||
def invoke(self, context, event):
|
|
||||||
return context.window_manager.invoke_props_dialog(self)
|
|
||||||
|
|
||||||
def execute(self, context):
|
|
||||||
asset_libraries = context.preferences.filepaths.asset_libraries
|
|
||||||
|
|
||||||
ob = bpy.context.object
|
|
||||||
ntree = context.space_data.edit_tree
|
|
||||||
ntree_name = ntree.name
|
|
||||||
new_ntree = None
|
|
||||||
|
|
||||||
if self.selection == 'OBJECT':
|
|
||||||
materials = [s.material for s in ob.material_slots if s.material]
|
|
||||||
elif self.selection == 'CURRENT':
|
|
||||||
materials = [ob.active_material]
|
|
||||||
else:
|
|
||||||
materials = list(bpy.data.materials)
|
|
||||||
|
|
||||||
mat_names = set(m.name for m in materials)
|
|
||||||
|
|
||||||
for asset_library in asset_libraries:
|
|
||||||
library_path = Path(asset_library.path)
|
|
||||||
blend_files = [fp for fp in library_path.glob("**/*.blend") if fp.is_file()]
|
|
||||||
|
|
||||||
images = list(bpy.data.images)
|
|
||||||
materials = list(bpy.data.materials)# Storing original materials to compare with imported ones
|
|
||||||
|
|
||||||
link = (asset_library.import_method == 'LINK')
|
|
||||||
for blend_file in blend_files:
|
|
||||||
print(blend_file)
|
|
||||||
with bpy.data.libraries.load(str(blend_file), assets_only=True, link=link) as (data_from, data_to):
|
|
||||||
|
|
||||||
import_materials = [n for n in data_from.materials if n in mat_names]
|
|
||||||
print("import_materials", import_materials)
|
|
||||||
data_to.materials = import_materials
|
|
||||||
|
|
||||||
mat_names -= set(import_materials) # Store already updated nodes
|
|
||||||
|
|
||||||
new_materials = set(m for m in bpy.data.materials if m not in materials)
|
|
||||||
new_images = set(i for i in bpy.data.images if i not in images)
|
|
||||||
#
|
|
||||||
|
|
||||||
for new_mat in new_materials:
|
|
||||||
new_mat_name = new_mat.library_weak_reference.id_name[2:]
|
|
||||||
local_mat = next((m for m in bpy.data.materials if m.name == new_mat_name and m != new_mat), None)
|
|
||||||
|
|
||||||
if not local_mat:
|
|
||||||
print(f'No local material {new_mat_name}')
|
|
||||||
continue
|
|
||||||
|
|
||||||
print(f'Merge material {local_mat.name} into {new_mat.name}')
|
|
||||||
|
|
||||||
local_mat.user_remap(new_mat)
|
|
||||||
bpy.data.materials.remove(local_mat)
|
|
||||||
|
|
||||||
if not new_mat.library:
|
|
||||||
new_mat.name = new_mat_name
|
|
||||||
new_mat.asset_clear()
|
|
||||||
|
|
||||||
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
|
|
||||||
def draw(self, context):
|
|
||||||
layout = self.layout
|
|
||||||
layout.prop(self, "selection", expand=True)
|
|
||||||
|
|
||||||
|
|
||||||
bl_classes = (
|
|
||||||
ASSETLIB_OT_update_materials,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def register():
|
|
||||||
for bl_class in bl_classes:
|
|
||||||
bpy.utils.register_class(bl_class)
|
|
||||||
|
|
||||||
|
|
||||||
def unregister():
|
|
||||||
for bl_class in reversed(bl_classes):
|
|
||||||
bpy.utils.unregister_class(bl_class)
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
"""
|
|
||||||
This module contains blender UI elements in the node editor
|
|
||||||
|
|
||||||
:author: Autour de Minuit
|
|
||||||
:maintainers: Christophe Seux
|
|
||||||
:date: 2024
|
|
||||||
"""
|
|
||||||
|
|
||||||
import bpy
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def draw_menu(self, context):
|
|
||||||
layout = self.layout
|
|
||||||
|
|
||||||
row = layout.row(align=False)
|
|
||||||
layout.operator('assetlibrary.update_materials', text='Update Materials', icon='MATERIAL')
|
|
||||||
|
|
||||||
|
|
||||||
def register():
|
|
||||||
# for c in classes:
|
|
||||||
# bpy.utils.register_class(c)
|
|
||||||
|
|
||||||
bpy.types.ASSETLIB_MT_node_editor.append(draw_menu)
|
|
||||||
|
|
||||||
|
|
||||||
def unregister():
|
|
||||||
# for c in reversed(classes):
|
|
||||||
# bpy.utils.unregister_class(c)
|
|
||||||
|
|
||||||
bpy.types.ASSETLIB_MT_node_editor.remove(draw_menu)
|
|
||||||
@@ -1,138 +0,0 @@
|
|||||||
|
|
||||||
from pathlib import Path
|
|
||||||
from datetime import datetime
|
|
||||||
import subprocess
|
|
||||||
|
|
||||||
from ...core.bl_utils import get_bl_cmd
|
|
||||||
from ...core.lib_utils import get_asset_data, asset_library_map
|
|
||||||
from ...core.catalog import read_catalog
|
|
||||||
from ... import constants
|
|
||||||
|
|
||||||
import bpy
|
|
||||||
from bpy.types import Operator
|
|
||||||
from bpy.props import StringProperty, EnumProperty
|
|
||||||
|
|
||||||
|
|
||||||
class ASSETLIB_OT_update_nodes(Operator):
|
|
||||||
bl_idname = 'assetlibrary.update_nodes'
|
|
||||||
bl_label = 'Update node'
|
|
||||||
bl_options = {"REGISTER", "UNDO"}
|
|
||||||
|
|
||||||
selection : EnumProperty(items=[(s, s.title(), '') for s in ('ALL', 'SELECTED', 'CURRENT')], default="CURRENT", name='All Nodes')
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def poll(cls, context):
|
|
||||||
return context.space_data.edit_tree
|
|
||||||
|
|
||||||
def invoke(self, context, event):
|
|
||||||
return context.window_manager.invoke_props_dialog(self)
|
|
||||||
|
|
||||||
def execute(self, context):
|
|
||||||
asset_libraries = context.preferences.filepaths.asset_libraries
|
|
||||||
|
|
||||||
ntree = context.space_data.edit_tree
|
|
||||||
ntree_name = ntree.name
|
|
||||||
new_ntree = None
|
|
||||||
|
|
||||||
if self.selection == 'SELECTED':
|
|
||||||
nodes = [ n.node_tree for n in context.space_data.edit_tree.nodes
|
|
||||||
if n.type == "GROUP" and n.select]
|
|
||||||
elif self.selection == 'CURRENT':
|
|
||||||
active_node = context.space_data.edit_tree
|
|
||||||
nodes = [active_node]
|
|
||||||
else:
|
|
||||||
nodes = list(bpy.data.node_groups)
|
|
||||||
|
|
||||||
library_map = asset_library_map()
|
|
||||||
|
|
||||||
#node_groups = list(bpy.data.node_groups)
|
|
||||||
#images = list(bpy.data.images)
|
|
||||||
#materials = list(bpy.data.materials)
|
|
||||||
|
|
||||||
with remap_datablock_duplicates():
|
|
||||||
|
|
||||||
for node in nodes:
|
|
||||||
blend_file = find_asset_source(library_map, 'node_groups', node.name)
|
|
||||||
link = bool(node.library)
|
|
||||||
|
|
||||||
with bpy.data.libraries.load(str(blend_file), link=link) as (data_from, data_to):
|
|
||||||
data_to.node_groups = [node.name]
|
|
||||||
|
|
||||||
for new_node_group in new_node_groups:
|
|
||||||
new_node_group_name = new_node_group.library_weak_reference.id_name[2:]
|
|
||||||
local_node_group = next((n for n in bpy.data.node_groups if n.name == new_node_group_name and n != new_node_group), None)
|
|
||||||
|
|
||||||
if not local_node_group:
|
|
||||||
print(f'No local node_group {new_node_group_name}')
|
|
||||||
continue
|
|
||||||
|
|
||||||
print(f'Merge node {local_node_group.name} into {new_node_group.name}')
|
|
||||||
|
|
||||||
local_node_group.user_remap(new_node_group)
|
|
||||||
new_node_group.interface_update(context)
|
|
||||||
bpy.data.node_groups.remove(local_node_group)
|
|
||||||
|
|
||||||
new_node_group.name = new_node_group_name
|
|
||||||
new_node_group.asset_clear()
|
|
||||||
|
|
||||||
|
|
||||||
node_names = set(n.name for n in nodes)
|
|
||||||
|
|
||||||
for asset_library in asset_libraries:
|
|
||||||
library_path = Path(asset_library.path)
|
|
||||||
blend_files = [fp for fp in library_path.glob("**/*.blend") if fp.is_file()]
|
|
||||||
|
|
||||||
node_groups = list(bpy.data.node_groups)# Storing original node_geoup to compare with imported
|
|
||||||
|
|
||||||
link = (asset_library.import_method == 'LINK')
|
|
||||||
for blend_file in blend_files:
|
|
||||||
print(blend_file)
|
|
||||||
with bpy.data.libraries.load(str(blend_file), assets_only=True, link=link) as (data_from, data_to):
|
|
||||||
|
|
||||||
import_node_groups = [n for n in data_from.node_groups if n in node_names]
|
|
||||||
print("import_node_groups", import_node_groups)
|
|
||||||
data_to.node_groups = import_node_groups
|
|
||||||
|
|
||||||
node_names -= set(import_node_groups) # Store already updated nodes
|
|
||||||
|
|
||||||
new_node_groups = set(n for n in bpy.data.node_groups if n not in node_groups)
|
|
||||||
|
|
||||||
for new_node_group in new_node_groups:
|
|
||||||
new_node_group_name = new_node_group.library_weak_reference.id_name[2:]
|
|
||||||
local_node_group = next((n for n in bpy.data.node_groups if n.name == new_node_group_name and n != new_node_group), None)
|
|
||||||
|
|
||||||
if not local_node_group:
|
|
||||||
print(f'No local node_group {new_node_group_name}')
|
|
||||||
continue
|
|
||||||
|
|
||||||
print(f'Merge node {local_node_group.name} into {new_node_group.name}')
|
|
||||||
|
|
||||||
local_node_group.user_remap(new_node_group)
|
|
||||||
new_node_group.interface_update(context)
|
|
||||||
bpy.data.node_groups.remove(local_node_group)
|
|
||||||
|
|
||||||
new_node_group.name = new_node_group_name
|
|
||||||
new_node_group.asset_clear()
|
|
||||||
|
|
||||||
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
|
|
||||||
def draw(self, context):
|
|
||||||
layout = self.layout
|
|
||||||
layout.prop(self, "selection", expand=True)
|
|
||||||
|
|
||||||
|
|
||||||
bl_classes = (
|
|
||||||
ASSETLIB_OT_update_nodes,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def register():
|
|
||||||
for bl_class in bl_classes:
|
|
||||||
bpy.utils.register_class(bl_class)
|
|
||||||
|
|
||||||
|
|
||||||
def unregister():
|
|
||||||
for bl_class in reversed(bl_classes):
|
|
||||||
bpy.utils.unregister_class(bl_class)
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
"""
|
|
||||||
This module contains blender UI elements in the node editor
|
|
||||||
|
|
||||||
:author: Autour de Minuit
|
|
||||||
:maintainers: Christophe Seux
|
|
||||||
:date: 2024
|
|
||||||
"""
|
|
||||||
|
|
||||||
import bpy
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def draw_menu(self, context):
|
|
||||||
layout = self.layout
|
|
||||||
|
|
||||||
row = layout.row(align=False)
|
|
||||||
layout.operator('assetlibrary.update_nodes', text='Update Node Group', icon='IMPORT')
|
|
||||||
|
|
||||||
|
|
||||||
def register():
|
|
||||||
# for c in classes:
|
|
||||||
# bpy.utils.register_class(c)
|
|
||||||
|
|
||||||
bpy.types.ASSETLIB_MT_node_editor.append(draw_menu)
|
|
||||||
|
|
||||||
|
|
||||||
def unregister():
|
|
||||||
# for c in reversed(classes):
|
|
||||||
# bpy.utils.unregister_class(c)
|
|
||||||
|
|
||||||
bpy.types.ASSETLIB_MT_node_editor.remove(draw_menu)
|
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
|
from asset_library.file import operators, gui, keymaps
|
||||||
|
|
||||||
from asset_library.data_type.file import (
|
if "bpy" in locals():
|
||||||
operators, gui, keymaps)
|
|
||||||
|
|
||||||
if 'bpy' in locals():
|
|
||||||
import importlib
|
import importlib
|
||||||
|
|
||||||
importlib.reload(operators)
|
importlib.reload(operators)
|
||||||
@@ -11,10 +9,12 @@ if 'bpy' in locals():
|
|||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
|
|
||||||
|
|
||||||
def register():
|
def register():
|
||||||
operators.register()
|
operators.register()
|
||||||
keymaps.register()
|
keymaps.register()
|
||||||
|
|
||||||
|
|
||||||
def unregister():
|
def unregister():
|
||||||
operators.unregister()
|
operators.unregister()
|
||||||
keymaps.unregister()
|
keymaps.unregister()
|
||||||
+177
@@ -0,0 +1,177 @@
|
|||||||
|
import argparse
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
import bpy
|
||||||
|
import re
|
||||||
|
import uuid
|
||||||
|
from itertools import groupby
|
||||||
|
|
||||||
|
from asset_library.constants import ASSETLIB_FILENAME, MODULE_DIR
|
||||||
|
from asset_library.common.bl_utils import thumbnail_blend_file
|
||||||
|
from asset_library.common.functions import command
|
||||||
|
|
||||||
|
|
||||||
|
@command
|
||||||
|
def bundle_library(
|
||||||
|
source_directory,
|
||||||
|
bundle_directory,
|
||||||
|
template_info,
|
||||||
|
thumbnail_template,
|
||||||
|
template=None,
|
||||||
|
data_file=None,
|
||||||
|
):
|
||||||
|
|
||||||
|
field_pattern = r"{(\w+)}"
|
||||||
|
asset_data_path = Path(bundle_directory) / ASSETLIB_FILENAME
|
||||||
|
|
||||||
|
glob_pattern = re.sub(field_pattern, "*", template)
|
||||||
|
re_pattern = re.sub(field_pattern, r"([\\w -_.]+)", template)
|
||||||
|
re_pattern = re_pattern.replace("?", ".")
|
||||||
|
|
||||||
|
field_names = re.findall(field_pattern, template)
|
||||||
|
|
||||||
|
asset_file_datas = []
|
||||||
|
for f in sorted(Path(source_directory).glob(glob_pattern)):
|
||||||
|
rel_path = f.relative_to(source_directory).as_posix()
|
||||||
|
|
||||||
|
field_values = re.findall(re_pattern, rel_path)[0]
|
||||||
|
field_data = {k: v for k, v in zip(field_names, field_values)}
|
||||||
|
|
||||||
|
name = field_data.get("name", f.stem)
|
||||||
|
thumbnail = (f / thumbnail_template.format(name=name)).resolve()
|
||||||
|
asset_data = (f / template_info.format(name=name)).resolve()
|
||||||
|
|
||||||
|
catalogs = sorted(
|
||||||
|
[v for k, v in sorted(field_data.items()) if re.findall("cat[0-9]+", k)]
|
||||||
|
)
|
||||||
|
catalogs = [c.replace("_", " ").title() for c in catalogs]
|
||||||
|
|
||||||
|
if not thumbnail.exists():
|
||||||
|
thumbnail_blend_file(f, thumbnail)
|
||||||
|
|
||||||
|
asset_data = {
|
||||||
|
"catalog": "/".join(catalogs),
|
||||||
|
"preview": thumbnail.as_posix(), #'./' + bpy.path.relpath(str(thumbnail), start=str(f))[2:],
|
||||||
|
"filepath": f.as_posix(), #'./' + bpy.path.relpath(str(f), start=str(asset_data_path))[2:],
|
||||||
|
"name": name,
|
||||||
|
"tags": [],
|
||||||
|
"metadata": {"filepath": f.as_posix()},
|
||||||
|
}
|
||||||
|
|
||||||
|
asset_file_datas.append(asset_data)
|
||||||
|
|
||||||
|
# Write json data file to store all asset found
|
||||||
|
print(f"Writing asset data file to, {asset_data_path}")
|
||||||
|
asset_data_path.write_text(json.dumps(asset_file_datas, indent=4))
|
||||||
|
|
||||||
|
# script = MODULE_DIR / 'common' / 'bundle_blend.py'
|
||||||
|
# cmd = [bpy.app.binary_path, '--python', str(script), '--', '--filepath', str(filepath)]
|
||||||
|
# print(cmd)
|
||||||
|
# subprocess.call(cmd)
|
||||||
|
|
||||||
|
|
||||||
|
@command
|
||||||
|
def bundle_blend(filepath, depth=0):
|
||||||
|
# print('Bundle Blend...')
|
||||||
|
filepath = Path(filepath)
|
||||||
|
|
||||||
|
# asset_data_path = get_asset_datas_file(filepath)
|
||||||
|
|
||||||
|
asset_data_path = filepath / ASSETLIB_FILENAME
|
||||||
|
blend_name = filepath.name.replace(" ", "_").lower()
|
||||||
|
blend_path = (filepath / blend_name).with_suffix(".blend")
|
||||||
|
|
||||||
|
if not asset_data_path.exists():
|
||||||
|
raise Exception(f"The file {asset_data_path} not exist")
|
||||||
|
|
||||||
|
catalog_path = get_catalog_path(filepath)
|
||||||
|
catalog_data = read_catalog(catalog_path)
|
||||||
|
|
||||||
|
asset_file_data = json.loads(asset_data_path.read_text())
|
||||||
|
# asset_file_data = {i['catalog']:i for i in asset_file_data}
|
||||||
|
|
||||||
|
if depth == 0:
|
||||||
|
groups = [asset_file_data]
|
||||||
|
else:
|
||||||
|
asset_file_data.sort(key=lambda x: x["catalog"].split("/")[:depth])
|
||||||
|
groups = groupby(asset_file_data, key=lambda x: x["catalog"].split("/")[:depth])
|
||||||
|
|
||||||
|
# progress = 0
|
||||||
|
total_assets = len(asset_file_data)
|
||||||
|
|
||||||
|
i = 0
|
||||||
|
for sub_path, asset_datas in groups:
|
||||||
|
bpy.ops.wm.read_homefile(use_empty=True)
|
||||||
|
|
||||||
|
for asset_data in asset_datas:
|
||||||
|
blend_name = sub_path[-1].replace(" ", "_").lower()
|
||||||
|
blend_path = Path(filepath, *sub_path, blend_name).with_suffix(".blend")
|
||||||
|
|
||||||
|
if i % int(total_assets / 100) == 0:
|
||||||
|
print(f"Progress: {int(i / total_assets * 100)}")
|
||||||
|
|
||||||
|
col = bpy.data.collections.new(name=asset_data["name"])
|
||||||
|
|
||||||
|
# Seems slow
|
||||||
|
# bpy.context.scene.collection.children.link(col)
|
||||||
|
col.asset_mark()
|
||||||
|
|
||||||
|
with bpy.context.temp_override(id=col):
|
||||||
|
bpy.ops.ed.lib_id_load_custom_preview(filepath=asset_data["preview"])
|
||||||
|
|
||||||
|
col.asset_data.description = asset_data.get("description", "")
|
||||||
|
|
||||||
|
catalog_name = asset_data["catalog"]
|
||||||
|
catalog = catalog_data.get(catalog_name)
|
||||||
|
if not catalog:
|
||||||
|
catalog = {"id": str(uuid.uuid4()), "name": catalog_name}
|
||||||
|
catalog_data[catalog_name] = catalog
|
||||||
|
|
||||||
|
col.asset_data.catalog_id = catalog["id"]
|
||||||
|
|
||||||
|
for k, v in asset_data.get("metadata", {}).items():
|
||||||
|
col.asset_data[k] = v
|
||||||
|
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
print(f"Saving Blend to {blend_path}")
|
||||||
|
|
||||||
|
blend_path.mkdir(exist_ok=True, parents=True)
|
||||||
|
bpy.ops.wm.save_as_mainfile(filepath=str(blend_path), compress=True)
|
||||||
|
|
||||||
|
write_catalog(catalog_path, catalog_data)
|
||||||
|
|
||||||
|
bpy.ops.wm.quit_blender()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="bundle_blend",
|
||||||
|
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument("--source-path")
|
||||||
|
parser.add_argument("--bundle-path")
|
||||||
|
parser.add_argument("--asset-data-template")
|
||||||
|
parser.add_argument("--thumbnail-template")
|
||||||
|
parser.add_argument("--template", default=None)
|
||||||
|
parser.add_argument("--data-file", default=None)
|
||||||
|
parser.add_argument("--depth", default=0, type=int)
|
||||||
|
|
||||||
|
if "--" in sys.argv:
|
||||||
|
index = sys.argv.index("--")
|
||||||
|
sys.argv = [sys.argv[index - 1], *sys.argv[index + 1 :]]
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
bundle_library(
|
||||||
|
source_directory=args.source_directory,
|
||||||
|
bundle_directory=args.bundle_directory,
|
||||||
|
template_info=args.template_info,
|
||||||
|
thumbnail_template=args.thumbnail_template,
|
||||||
|
template=args.template,
|
||||||
|
data_file=args.data_file,
|
||||||
|
)
|
||||||
|
|
||||||
|
bundle_blend(filepath=args.bundle_directory, depth=args.depth)
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@@ -18,12 +17,14 @@ from asset_library.common.functions import get_active_library
|
|||||||
|
|
||||||
|
|
||||||
def draw_context_menu(layout):
|
def draw_context_menu(layout):
|
||||||
#asset = context.active_file
|
# asset = context.active_file
|
||||||
layout.operator_context = "INVOKE_DEFAULT"
|
layout.operator_context = "INVOKE_DEFAULT"
|
||||||
lib = get_active_library()
|
lib = get_active_library()
|
||||||
filepath = lib.library_type.get_active_asset_path()
|
filepath = lib.library_type.get_active_asset_path()
|
||||||
|
|
||||||
layout.operator("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 = layout.operator("wm.link", text="Link")
|
||||||
op.filepath = str(filepath)
|
op.filepath = str(filepath)
|
||||||
|
|
||||||
@@ -32,7 +33,7 @@ def draw_context_menu(layout):
|
|||||||
|
|
||||||
|
|
||||||
def draw_header(layout):
|
def draw_header(layout):
|
||||||
'''Draw the header of the Asset Browser Window'''
|
"""Draw the header of the Asset Browser Window"""
|
||||||
|
|
||||||
layout.separator()
|
layout.separator()
|
||||||
#layout.operator("actionlib.store_anim_pose", text='Add Action', icon='FILE_NEW')
|
# layout.operator("actionlib.store_anim_pose", text='Add Action', icon='FILE_NEW')
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
|
|
||||||
|
|
||||||
from typing import List, Tuple
|
from typing import List, Tuple
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
@@ -7,13 +5,16 @@ from bpy.app.handlers import persistent
|
|||||||
|
|
||||||
addon_keymaps: List[Tuple[bpy.types.KeyMap, bpy.types.KeyMapItem]] = []
|
addon_keymaps: List[Tuple[bpy.types.KeyMap, bpy.types.KeyMapItem]] = []
|
||||||
|
|
||||||
|
|
||||||
def register() -> None:
|
def register() -> None:
|
||||||
wm = bpy.context.window_manager
|
wm = bpy.context.window_manager
|
||||||
if not wm.keyconfigs.addon:
|
if not wm.keyconfigs.addon:
|
||||||
# This happens when Blender is running in the background.
|
# This happens when Blender is running in the background.
|
||||||
return
|
return
|
||||||
|
|
||||||
km = wm.keyconfigs.addon.keymaps.new(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")
|
kmi = km.keymap_items.new("assetlib.open_blend_file", "LEFTMOUSE", "DOUBLE_CLICK")
|
||||||
addon_keymaps.append((km, kmi))
|
addon_keymaps.append((km, kmi))
|
||||||
@@ -1,12 +1,14 @@
|
|||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
from bpy.types import Context, Operator
|
from bpy.types import Context, Operator
|
||||||
from bpy_extras import asset_utils
|
from bpy_extras import asset_utils
|
||||||
from bpy.props import StringProperty
|
from bpy.props import StringProperty
|
||||||
from typing import List, Tuple, Set
|
from typing import List, Tuple, Set
|
||||||
|
|
||||||
from asset_library.common.file_utils import (open_blender_file,
|
from asset_library.common.file_utils import (
|
||||||
synchronize, open_blender_file)
|
open_blender_file,
|
||||||
|
synchronize,
|
||||||
|
open_blender_file,
|
||||||
|
)
|
||||||
|
|
||||||
from asset_library.common.functions import get_active_library
|
from asset_library.common.functions import get_active_library
|
||||||
|
|
||||||
@@ -14,8 +16,8 @@ from asset_library.common.functions import get_active_library
|
|||||||
class ASSETLIB_OT_open_blend_file(Operator):
|
class ASSETLIB_OT_open_blend_file(Operator):
|
||||||
bl_idname = "assetlib.open_blend_file"
|
bl_idname = "assetlib.open_blend_file"
|
||||||
bl_options = {"REGISTER", "UNDO"}
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
bl_label = 'Open Blender File'
|
bl_label = "Open Blender File"
|
||||||
bl_description = 'Open blender file'
|
bl_description = "Open blender file"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context: Context) -> bool:
|
def poll(cls, context: Context) -> bool:
|
||||||
@@ -24,10 +26,10 @@ class ASSETLIB_OT_open_blend_file(Operator):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
lib = get_active_library()
|
lib = get_active_library()
|
||||||
if not lib or lib.data_type != 'FILE':
|
if not lib or lib.data_type != "FILE":
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if not context.active_file or 'filepath' not in context.active_file.asset_data:
|
if not context.active_file or "filepath" not in context.active_file.asset_data:
|
||||||
cls.poll_message_set("Has not filepath property")
|
cls.poll_message_set("Has not filepath property")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -41,17 +43,17 @@ class ASSETLIB_OT_open_blend_file(Operator):
|
|||||||
|
|
||||||
open_blender_file(filepath)
|
open_blender_file(filepath)
|
||||||
|
|
||||||
return {'FINISHED'}
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
classes = (
|
classes = (ASSETLIB_OT_open_blend_file,)
|
||||||
ASSETLIB_OT_open_blend_file,
|
|
||||||
)
|
|
||||||
|
|
||||||
def register():
|
def register():
|
||||||
for cls in classes:
|
for cls in classes:
|
||||||
bpy.utils.register_class(cls)
|
bpy.utils.register_class(cls)
|
||||||
|
|
||||||
|
|
||||||
def unregister():
|
def unregister():
|
||||||
for cls in reversed(classes):
|
for cls in reversed(classes):
|
||||||
bpy.utils.unregister_class(cls)
|
bpy.utils.unregister_class(cls)
|
||||||
Binary file not shown.
@@ -0,0 +1,343 @@
|
|||||||
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
Action Library - GUI definition.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import bpy
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from bpy.types import (
|
||||||
|
AssetHandle,
|
||||||
|
Context,
|
||||||
|
Header,
|
||||||
|
Menu,
|
||||||
|
Panel,
|
||||||
|
UIList,
|
||||||
|
WindowManager,
|
||||||
|
WorkSpace,
|
||||||
|
)
|
||||||
|
|
||||||
|
from bpy_extras import asset_utils
|
||||||
|
from asset_library.common.bl_utils import (
|
||||||
|
get_addon_prefs,
|
||||||
|
get_object_libraries,
|
||||||
|
)
|
||||||
|
|
||||||
|
from asset_library.common.functions import get_active_library
|
||||||
|
|
||||||
|
|
||||||
|
def pose_library_panel_poll():
|
||||||
|
return bpy.context.object and bpy.context.object.mode == "POSE"
|
||||||
|
|
||||||
|
|
||||||
|
class PoseLibraryPanel:
|
||||||
|
@classmethod
|
||||||
|
def pose_library_panel_poll(cls, context: Context) -> bool:
|
||||||
|
return bool(context.object and context.object.mode == "POSE")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context: Context) -> bool:
|
||||||
|
return cls.pose_library_panel_poll(context)
|
||||||
|
|
||||||
|
|
||||||
|
class AssetLibraryMenu:
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
from bpy_extras.asset_utils import SpaceAssetInfo
|
||||||
|
|
||||||
|
return SpaceAssetInfo.is_asset_browser_poll(context)
|
||||||
|
|
||||||
|
|
||||||
|
class ASSETLIB_PT_libraries(Panel):
|
||||||
|
bl_label = "Libraries"
|
||||||
|
bl_space_type = "VIEW_3D"
|
||||||
|
bl_region_type = "UI"
|
||||||
|
bl_category = "Item"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context: Context) -> bool:
|
||||||
|
return context.object and get_object_libraries(context.object)
|
||||||
|
|
||||||
|
def draw(self, context: Context) -> None:
|
||||||
|
layout = self.layout
|
||||||
|
|
||||||
|
for f in get_object_libraries(context.object):
|
||||||
|
row = layout.row(align=True)
|
||||||
|
row.label(text=f)
|
||||||
|
row.operator("assetlib.open_blend", icon="FILE_BLEND", text="").filepath = f
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
class ASSETLIB_PT_pose_library_usage(Panel):
|
||||||
|
bl_space_type = 'FILE_BROWSER'
|
||||||
|
bl_region_type = "TOOLS"
|
||||||
|
bl_label = "Action Library"
|
||||||
|
# asset_categories = {'ANIMATIONS'}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context: Context) -> bool:
|
||||||
|
sp = context.space_data
|
||||||
|
|
||||||
|
if not context.object or not context.object.mode == 'POSE':
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not (sp and sp.type == 'FILE_BROWSER' and sp.browse_mode == 'ASSETS'):
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def draw(self, context: Context) -> None:
|
||||||
|
layout = self.layout
|
||||||
|
wm = context.window_manager
|
||||||
|
|
||||||
|
sp = context.space_data
|
||||||
|
sp.params.asset_library_ref
|
||||||
|
|
||||||
|
if sp.params.asset_library_ref == 'LOCAL':
|
||||||
|
col = layout.column(align=True)
|
||||||
|
row = col.row(align=True)
|
||||||
|
row.operator("poselib.create_pose_asset", text="Create Pose", icon='POSE_HLT').activate_new_action = False
|
||||||
|
row.operator("actionlib.replace_pose", text='Replace Pose', icon='FILE_REFRESH')
|
||||||
|
col.operator("actionlib.create_anim_asset", text="Create Anim", icon='ANIM')
|
||||||
|
|
||||||
|
col.separator()
|
||||||
|
row = col.row(align=True)
|
||||||
|
row.operator("actionlib.edit_action", text='Edit Action', icon='ACTION')
|
||||||
|
row.operator("actionlib.clear_action", text='Finish Edit', icon='CHECKBOX_HLT')
|
||||||
|
|
||||||
|
col.separator()
|
||||||
|
col.operator("actionlib.generate_preview", icon='RESTRICT_RENDER_OFF', text="Generate Thumbnail")
|
||||||
|
col.operator("actionlib.update_action_data", icon='FILE_TEXT', text="Update Action Data")
|
||||||
|
else:
|
||||||
|
col = layout.column(align=True)
|
||||||
|
row = col.row(align=True)
|
||||||
|
row.operator("actionlib.store_anim_pose", text='Store Anim/Pose', icon='ACTION')
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class ASSETLIB_PT_pose_library_editing(
|
||||||
|
PoseLibraryPanel, asset_utils.AssetBrowserPanel, Panel
|
||||||
|
):
|
||||||
|
bl_space_type = "FILE_BROWSER"
|
||||||
|
bl_region_type = "TOOL_PROPS"
|
||||||
|
bl_label = "Metadata"
|
||||||
|
# bl_options = {'HIDE_HEADER'}
|
||||||
|
# asset_categories = {'ANIMATIONS'}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context: Context) -> bool:
|
||||||
|
sp = context.space_data
|
||||||
|
|
||||||
|
if not (sp and sp.type == "FILE_BROWSER" and sp.browse_mode == "ASSETS"):
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not (context.active_file and context.active_file.asset_data):
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def draw(self, context: Context) -> None:
|
||||||
|
layout = self.layout
|
||||||
|
|
||||||
|
layout.use_property_split = True
|
||||||
|
asset_data = context.active_file.asset_data
|
||||||
|
metadata = ["camera", "is_single_frame", "rest_pose"]
|
||||||
|
|
||||||
|
if "camera" in asset_data.keys():
|
||||||
|
layout.prop(asset_data, f'["camera"]', text="Camera", icon="CAMERA_DATA")
|
||||||
|
if "is_single_frame" in asset_data.keys():
|
||||||
|
layout.prop(asset_data, f'["is_single_frame"]', text="Is Single Frame")
|
||||||
|
if "rest_pose" in asset_data.keys():
|
||||||
|
layout.prop(asset_data, f'["rest_pose"]', text="Rest Pose", icon="ACTION")
|
||||||
|
if "filepath" in asset_data.keys():
|
||||||
|
layout.prop(asset_data, f'["filepath"]', text="Filepath")
|
||||||
|
|
||||||
|
|
||||||
|
class ASSETLIB_MT_context_menu(AssetLibraryMenu, Menu):
|
||||||
|
bl_label = "Asset Library Menu"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
if not asset_utils.SpaceAssetInfo.is_asset_browser(context.space_data):
|
||||||
|
cls.poll_message_set("Current editor is not an asset browser")
|
||||||
|
return False
|
||||||
|
|
||||||
|
prefs = get_addon_prefs()
|
||||||
|
asset_lib_ref = context.space_data.params.asset_library_ref
|
||||||
|
|
||||||
|
lib = get_active_library()
|
||||||
|
if not lib:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def draw(self, context):
|
||||||
|
lib = get_active_library()
|
||||||
|
lib.library_type.draw_context_menu(self.layout)
|
||||||
|
|
||||||
|
|
||||||
|
def is_option_region_visible(context, space):
|
||||||
|
from bpy_extras.asset_utils import SpaceAssetInfo
|
||||||
|
|
||||||
|
if SpaceAssetInfo.is_asset_browser(space):
|
||||||
|
pass
|
||||||
|
# For the File Browser, there must be an operator for there to be options
|
||||||
|
# (irrelevant for the Asset Browser).
|
||||||
|
elif not space.active_operator:
|
||||||
|
return False
|
||||||
|
|
||||||
|
for region in context.area.regions:
|
||||||
|
if region.type == "TOOL_PROPS" and region.width <= 1:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def draw_assetbrowser_header(self, context):
|
||||||
|
lib = get_active_library()
|
||||||
|
|
||||||
|
if not lib:
|
||||||
|
bpy.types.FILEBROWSER_HT_header._draw_asset_browser_buttons(self, context)
|
||||||
|
return
|
||||||
|
|
||||||
|
space_data = context.space_data
|
||||||
|
params = context.space_data.params
|
||||||
|
|
||||||
|
row = self.layout.row(align=True)
|
||||||
|
row.separator()
|
||||||
|
|
||||||
|
row.operator("assetlib.bundle", icon="UV_SYNC_SELECT", text="").name = lib.name
|
||||||
|
# op
|
||||||
|
# op.clean = False
|
||||||
|
# op.only_recent = True
|
||||||
|
|
||||||
|
lib.library_type.draw_header(row)
|
||||||
|
|
||||||
|
if context.selected_files and context.active_file:
|
||||||
|
row.separator()
|
||||||
|
row.label(text=context.active_file.name)
|
||||||
|
|
||||||
|
row.separator_spacer()
|
||||||
|
|
||||||
|
sub = row.row()
|
||||||
|
sub.ui_units_x = 10
|
||||||
|
sub.prop(params, "filter_search", text="", icon="VIEWZOOM")
|
||||||
|
|
||||||
|
row.separator_spacer()
|
||||||
|
|
||||||
|
row.prop_with_popover(
|
||||||
|
params,
|
||||||
|
"display_type",
|
||||||
|
panel="ASSETBROWSER_PT_display",
|
||||||
|
text="",
|
||||||
|
icon_only=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
row.operator(
|
||||||
|
"screen.region_toggle",
|
||||||
|
text="",
|
||||||
|
icon="PREFERENCES",
|
||||||
|
depress=is_option_region_visible(context, space_data),
|
||||||
|
).region_type = "TOOL_PROPS"
|
||||||
|
|
||||||
|
|
||||||
|
### Messagebus subscription to monitor asset library changes.
|
||||||
|
_msgbus_owner = object()
|
||||||
|
|
||||||
|
|
||||||
|
def _on_asset_library_changed() -> None:
|
||||||
|
"""Update areas when a different asset library is selected."""
|
||||||
|
refresh_area_types = {"DOPESHEET_EDITOR", "VIEW_3D"}
|
||||||
|
for win in bpy.context.window_manager.windows:
|
||||||
|
for area in win.screen.areas:
|
||||||
|
if area.type not in refresh_area_types:
|
||||||
|
continue
|
||||||
|
|
||||||
|
area.tag_redraw()
|
||||||
|
|
||||||
|
|
||||||
|
def register_message_bus() -> None:
|
||||||
|
|
||||||
|
bpy.msgbus.subscribe_rna(
|
||||||
|
key=(bpy.types.FileAssetSelectParams, "asset_library_ref"),
|
||||||
|
owner=_msgbus_owner,
|
||||||
|
args=(),
|
||||||
|
notify=_on_asset_library_changed,
|
||||||
|
options={"PERSISTENT"},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def unregister_message_bus() -> None:
|
||||||
|
bpy.msgbus.clear_by_owner(_msgbus_owner)
|
||||||
|
|
||||||
|
|
||||||
|
@bpy.app.handlers.persistent
|
||||||
|
def _on_blendfile_load_pre(none, other_none) -> None:
|
||||||
|
# The parameters are required, but both are None.
|
||||||
|
unregister_message_bus()
|
||||||
|
|
||||||
|
|
||||||
|
@bpy.app.handlers.persistent
|
||||||
|
def _on_blendfile_load_post(none, other_none) -> None:
|
||||||
|
# The parameters are required, but both are None.
|
||||||
|
register_message_bus()
|
||||||
|
|
||||||
|
|
||||||
|
classes = (
|
||||||
|
ASSETLIB_PT_pose_library_editing,
|
||||||
|
# ASSETLIB_PT_pose_library_usage,
|
||||||
|
ASSETLIB_MT_context_menu,
|
||||||
|
ASSETLIB_PT_libraries,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def register() -> None:
|
||||||
|
for cls in classes:
|
||||||
|
bpy.utils.register_class(cls)
|
||||||
|
|
||||||
|
bpy.types.FILEBROWSER_HT_header._draw_asset_browser_buttons = (
|
||||||
|
bpy.types.FILEBROWSER_HT_header.draw_asset_browser_buttons
|
||||||
|
)
|
||||||
|
bpy.types.FILEBROWSER_HT_header.draw_asset_browser_buttons = (
|
||||||
|
draw_assetbrowser_header
|
||||||
|
)
|
||||||
|
|
||||||
|
# WorkSpace.active_pose_asset_index = bpy.props.IntProperty(
|
||||||
|
# name="Active Pose Asset",
|
||||||
|
# # TODO explain which list the index belongs to, or how it can be used to get the pose.
|
||||||
|
# description="Per workspace index of the active pose asset"
|
||||||
|
# )
|
||||||
|
# Register for window-manager. This is a global property that shouldn't be
|
||||||
|
# written to files.
|
||||||
|
# WindowManager.pose_assets = bpy.props.CollectionProperty(type=AssetHandle)
|
||||||
|
|
||||||
|
# bpy.types.UI_MT_list_item_context_menu.prepend(pose_library_list_item_context_menu)
|
||||||
|
# bpy.types.ASSETLIB_MT_context_menu.prepend(pose_library_list_item_context_menu)
|
||||||
|
# bpy.types.ACTIONLIB_MT_context_menu.prepend(pose_library_list_item_context_menu)
|
||||||
|
# bpy.types.ASSETBROWSER_MT_editor_menus.append(draw_assetbrowser_header)
|
||||||
|
|
||||||
|
register_message_bus()
|
||||||
|
bpy.app.handlers.load_pre.append(_on_blendfile_load_pre)
|
||||||
|
bpy.app.handlers.load_post.append(_on_blendfile_load_post)
|
||||||
|
|
||||||
|
|
||||||
|
def unregister() -> None:
|
||||||
|
for cls in reversed(classes):
|
||||||
|
bpy.utils.unregister_class(cls)
|
||||||
|
|
||||||
|
bpy.types.FILEBROWSER_HT_header.draw_asset_browser_buttons = (
|
||||||
|
bpy.types.FILEBROWSER_HT_header._draw_asset_browser_buttons
|
||||||
|
)
|
||||||
|
del bpy.types.FILEBROWSER_HT_header._draw_asset_browser_buttons
|
||||||
|
|
||||||
|
unregister_message_bus()
|
||||||
|
|
||||||
|
# del WorkSpace.active_pose_asset_index
|
||||||
|
# del WindowManager.pose_assets
|
||||||
|
|
||||||
|
# bpy.types.UI_MT_list_item_context_menu.remove(pose_library_list_item_context_menu)
|
||||||
|
# bpy.types.ASSETLIB_MT_context_menu.remove(pose_library_list_item_context_menu)
|
||||||
|
# bpy.types.ACTIONLIB_MT_context_menu.remove(pose_library_list_item_context_menu)
|
||||||
|
# bpy.types.ASSETBROWSER_MT_editor_menus.remove(draw_assetbrowser_header)
|
||||||
+68
@@ -0,0 +1,68 @@
|
|||||||
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
|
||||||
|
from typing import List, Tuple
|
||||||
|
|
||||||
|
import bpy
|
||||||
|
from bpy.app.handlers import persistent
|
||||||
|
|
||||||
|
addon_keymaps: List[Tuple[bpy.types.KeyMap, bpy.types.KeyMapItem]] = []
|
||||||
|
|
||||||
|
|
||||||
|
@persistent
|
||||||
|
def copy_play_anim(dummy):
|
||||||
|
wm = bpy.context.window_manager
|
||||||
|
km = wm.keyconfigs.addon.keymaps.new(
|
||||||
|
name="File Browser Main", space_type="FILE_BROWSER"
|
||||||
|
)
|
||||||
|
|
||||||
|
km_frames = wm.keyconfigs.user.keymaps.get("Frames")
|
||||||
|
if km_frames:
|
||||||
|
play = km_frames.keymap_items.get("screen.animation_play")
|
||||||
|
if play:
|
||||||
|
kmi = km.keymap_items.new(
|
||||||
|
"assetlib.play_preview",
|
||||||
|
play.type,
|
||||||
|
play.value,
|
||||||
|
any=play.any,
|
||||||
|
shift=play.shift,
|
||||||
|
ctrl=play.ctrl,
|
||||||
|
alt=play.alt,
|
||||||
|
oskey=play.oskey,
|
||||||
|
key_modifier=play.key_modifier,
|
||||||
|
)
|
||||||
|
addon_keymaps.append((km, kmi))
|
||||||
|
|
||||||
|
|
||||||
|
def register() -> None:
|
||||||
|
wm = bpy.context.window_manager
|
||||||
|
if wm.keyconfigs.addon is None:
|
||||||
|
# This happens when Blender is running in the background.
|
||||||
|
return
|
||||||
|
|
||||||
|
km = wm.keyconfigs.addon.keymaps.new(
|
||||||
|
name="File Browser Main", space_type="FILE_BROWSER"
|
||||||
|
)
|
||||||
|
|
||||||
|
kmi = km.keymap_items.new("wm.call_menu", "RIGHTMOUSE", "PRESS")
|
||||||
|
kmi.properties.name = "ASSETLIB_MT_context_menu"
|
||||||
|
addon_keymaps.append((km, kmi))
|
||||||
|
|
||||||
|
kmi = km.keymap_items.new("assetlib.play_preview", "SPACE", "PRESS")
|
||||||
|
addon_keymaps.append((km, kmi))
|
||||||
|
|
||||||
|
# km = addon.keymaps.new(name = "Grease Pencil Stroke Paint Mode", space_type = "EMPTY")
|
||||||
|
# kmi = km.keymap_items.new('wm.call_panel', type='F2', value='PRESS')
|
||||||
|
|
||||||
|
if "copy_play_anim" not in [hand.__name__ for hand in bpy.app.handlers.load_post]:
|
||||||
|
bpy.app.handlers.load_post.append(copy_play_anim)
|
||||||
|
|
||||||
|
|
||||||
|
def unregister() -> None:
|
||||||
|
# Clear shortcuts from the keymap.
|
||||||
|
if "copy_play_anim" in [hand.__name__ for hand in bpy.app.handlers.load_post]:
|
||||||
|
bpy.app.handlers.load_post.remove(copy_play_anim)
|
||||||
|
|
||||||
|
for km, kmi in addon_keymaps:
|
||||||
|
km.keymap_items.remove(kmi)
|
||||||
|
addon_keymaps.clear()
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
from asset_library.library_types import library_type
|
||||||
|
from asset_library.library_types import copy_folder
|
||||||
|
from asset_library.library_types import scan_folder
|
||||||
|
|
||||||
|
if "bpy" in locals():
|
||||||
|
import importlib
|
||||||
|
|
||||||
|
importlib.reload(library_type)
|
||||||
|
importlib.reload(copy_folder)
|
||||||
|
importlib.reload(scan_folder)
|
||||||
|
|
||||||
|
import bpy
|
||||||
|
|
||||||
|
LibraryType = library_type.LibraryType
|
||||||
|
CopyFolder = copy_folder.CopyFolder
|
||||||
|
ScanFolder = scan_folder.ScanFolder
|
||||||
@@ -1,15 +1,13 @@
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
Plugin for making an asset library of all blender file found in a folder
|
Plugin for making an asset library of all blender file found in a folder
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from asset_library.library_types.scan_folder import ScanFolder
|
||||||
from asset_library.plugins.scan_folder import ScanFolder
|
from asset_library.common.bl_utils import load_datablocks
|
||||||
from asset_library.core.bl_utils import load_datablocks
|
from asset_library.common.template import Template
|
||||||
from asset_library.core.template import Template
|
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
from bpy.props import (StringProperty, IntProperty, BoolProperty)
|
from bpy.props import StringProperty, IntProperty, BoolProperty
|
||||||
import re
|
import re
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from itertools import groupby
|
from itertools import groupby
|
||||||
@@ -24,27 +22,29 @@ from pprint import pprint
|
|||||||
class Conform(ScanFolder):
|
class Conform(ScanFolder):
|
||||||
|
|
||||||
name = "Conform"
|
name = "Conform"
|
||||||
source_directory : StringProperty(subtype='DIR_PATH')
|
source_directory: StringProperty(subtype="DIR_PATH")
|
||||||
|
|
||||||
target_template_file : StringProperty()
|
target_template_file: StringProperty()
|
||||||
target_template_info : StringProperty()
|
target_template_info: StringProperty()
|
||||||
target_template_image : StringProperty()
|
target_template_image: StringProperty()
|
||||||
target_template_video : StringProperty()
|
target_template_video: StringProperty()
|
||||||
|
|
||||||
def draw_prefs(self, layout):
|
def draw_prefs(self, layout):
|
||||||
layout.prop(self, "source_directory", text="Source : Directory")
|
layout.prop(self, "source_directory", text="Source : Directory")
|
||||||
|
|
||||||
col = layout.column(align=True)
|
col = layout.column(align=True)
|
||||||
col.prop(self, "source_template_file", icon='COPY_ID', text='Template file')
|
col.prop(self, "source_template_file", icon="COPY_ID", text="Template file")
|
||||||
col.prop(self, "source_template_image", icon='COPY_ID', text='Template image')
|
col.prop(self, "source_template_image", icon="COPY_ID", text="Template image")
|
||||||
col.prop(self, "source_template_video", icon='COPY_ID', text='Template video')
|
col.prop(self, "source_template_video", icon="COPY_ID", text="Template video")
|
||||||
col.prop(self, "source_template_info", icon='COPY_ID', text='Template info')
|
col.prop(self, "source_template_info", icon="COPY_ID", text="Template info")
|
||||||
|
|
||||||
col = layout.column(align=True)
|
col = layout.column(align=True)
|
||||||
col.prop(self, "target_template_file", icon='COPY_ID', text='Target : Template file')
|
col.prop(
|
||||||
col.prop(self, "target_template_image", icon='COPY_ID', text='Template image')
|
self, "target_template_file", icon="COPY_ID", text="Target : Template file"
|
||||||
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_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):
|
def get_asset_bundle_path(self, asset_data):
|
||||||
"""Template file are relative"""
|
"""Template file are relative"""
|
||||||
@@ -52,26 +52,30 @@ class Conform(ScanFolder):
|
|||||||
src_directory = Path(self.source_directory).resolve()
|
src_directory = Path(self.source_directory).resolve()
|
||||||
src_template_file = Template(self.source_template_file)
|
src_template_file = Template(self.source_template_file)
|
||||||
|
|
||||||
asset_path = Path(asset_data['filepath']).as_posix()
|
asset_path = Path(asset_data["filepath"]).as_posix()
|
||||||
asset_path = self.format_path(asset_path)
|
asset_path = self.format_path(asset_path)
|
||||||
|
|
||||||
rel_path = asset_path.relative_to(src_directory).as_posix()
|
rel_path = asset_path.relative_to(src_directory).as_posix()
|
||||||
field_data = src_template_file.parse(rel_path)
|
field_data = src_template_file.parse(rel_path)
|
||||||
#field_data = {f"catalog_{k}": v for k, v in field_data.items()}
|
# field_data = {f"catalog_{k}": v for k, v in field_data.items()}
|
||||||
|
|
||||||
# Change the int in the template by string to allow format
|
# Change the int in the template by string to allow format
|
||||||
#target_template_file = re.sub(r'{(\d+)}', r'{cat\1}', self.target_template_file)
|
# target_template_file = re.sub(r'{(\d+)}', r'{cat\1}', self.target_template_file)
|
||||||
|
|
||||||
format_data = self.format_asset_data(asset_data)
|
format_data = self.format_asset_data(asset_data)
|
||||||
#format_data['asset_name'] = format_data['asset_name'].lower().replace(' ', '_')
|
# format_data['asset_name'] = format_data['asset_name'].lower().replace(' ', '_')
|
||||||
|
|
||||||
path = 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()
|
path = Path(self.bundle_directory, path).resolve()
|
||||||
|
|
||||||
return path
|
return path
|
||||||
|
|
||||||
def set_asset_preview(self, asset, asset_data):
|
def set_asset_preview(self, asset, asset_data):
|
||||||
'''Load an externalize image as preview for an asset using the target template'''
|
"""Load an externalize image as preview for an asset using the target template"""
|
||||||
|
|
||||||
image_template = self.target_template_image
|
image_template = self.target_template_image
|
||||||
if not image_template:
|
if not image_template:
|
||||||
@@ -82,18 +86,16 @@ class Conform(ScanFolder):
|
|||||||
|
|
||||||
if image_path:
|
if image_path:
|
||||||
with bpy.context.temp_override(id=asset):
|
with bpy.context.temp_override(id=asset):
|
||||||
bpy.ops.ed.lib_id_load_custom_preview(
|
bpy.ops.ed.lib_id_load_custom_preview(filepath=str(image_path))
|
||||||
filepath=str(image_path)
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
print(f'No image found for {image_template} on {asset.name}')
|
print(f"No image found for {image_template} on {asset.name}")
|
||||||
|
|
||||||
if asset.preview:
|
if asset.preview:
|
||||||
return asset.preview
|
return asset.preview
|
||||||
|
|
||||||
def generate_previews(self, cache_diff):
|
def generate_previews(self, cache_diff):
|
||||||
|
|
||||||
print('Generate previews...')
|
print("Generate previews...")
|
||||||
|
|
||||||
# if cache in (None, ''):
|
# if cache in (None, ''):
|
||||||
# cache = self.fetch()
|
# cache = self.fetch()
|
||||||
@@ -103,12 +105,10 @@ class Conform(ScanFolder):
|
|||||||
if isinstance(cache, (Path, str)):
|
if isinstance(cache, (Path, str)):
|
||||||
cache_diff = LibraryCacheDiff(cache_diff)
|
cache_diff = LibraryCacheDiff(cache_diff)
|
||||||
|
|
||||||
|
# TODO Support all multiple data_type
|
||||||
|
|
||||||
#TODO Support all multiple data_type
|
|
||||||
for asset_info in cache:
|
for asset_info in cache:
|
||||||
|
|
||||||
if asset_info.get('type', self.data_type) == 'FILE':
|
if asset_info.get("type", self.data_type) == "FILE":
|
||||||
self.generate_blend_preview(asset_info)
|
self.generate_blend_preview(asset_info)
|
||||||
else:
|
else:
|
||||||
self.generate_asset_preview(asset_info)
|
self.generate_asset_preview(asset_info)
|
||||||
@@ -116,49 +116,55 @@ class Conform(ScanFolder):
|
|||||||
def generate_asset_preview(self, asset_info):
|
def generate_asset_preview(self, asset_info):
|
||||||
"""Only generate preview when conforming a library"""
|
"""Only generate preview when conforming a library"""
|
||||||
|
|
||||||
#print('\ngenerate_preview', asset_info['filepath'])
|
# print('\ngenerate_preview', asset_info['filepath'])
|
||||||
|
|
||||||
scn = bpy.context.scene
|
scn = bpy.context.scene
|
||||||
vl = bpy.context.view_layer
|
vl = bpy.context.view_layer
|
||||||
#Creating the preview for collection, object or material
|
# Creating the preview for collection, object or material
|
||||||
#camera = scn.camera
|
# camera = scn.camera
|
||||||
|
|
||||||
data_type = self.data_type #asset_info['data_type']
|
data_type = self.data_type # asset_info['data_type']
|
||||||
asset_path = self.format_path(asset_info['filepath'])
|
asset_path = self.format_path(asset_info["filepath"])
|
||||||
|
|
||||||
# Check if a source video exists and if so copying it in the new directory
|
# Check if a source video exists and if so copying it in the new directory
|
||||||
if self.source_template_video and self.target_template_video:
|
if self.source_template_video and self.target_template_video:
|
||||||
for asset_data in asset_info['assets']:
|
for asset_data in asset_info["assets"]:
|
||||||
asset_data = dict(asset_data, filepath=asset_path)
|
asset_data = dict(asset_data, filepath=asset_path)
|
||||||
|
|
||||||
dst_asset_path = self.get_asset_bundle_path(asset_data)
|
dst_asset_path = self.get_asset_bundle_path(asset_data)
|
||||||
dst_video_path = self.format_path(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():
|
if dst_video_path.exists():
|
||||||
print(f'The dest video {dst_video_path} already exist')
|
print(f"The dest video {dst_video_path} already exist")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
src_video_path = self.find_path(self.source_template_video, asset_data)
|
src_video_path = self.find_path(self.source_template_video, asset_data)
|
||||||
if src_video_path:
|
if src_video_path:
|
||||||
print(f'Copy video from {src_video_path} to {dst_video_path}')
|
print(f"Copy video from {src_video_path} to {dst_video_path}")
|
||||||
self.copy_file(src_video_path, dst_video_path)
|
self.copy_file(src_video_path, dst_video_path)
|
||||||
|
|
||||||
# Check if asset as a preview image or need it to be generated
|
# Check if asset as a preview image or need it to be generated
|
||||||
asset_data_names = {}
|
asset_data_names = {}
|
||||||
|
|
||||||
if self.target_template_image:
|
if self.target_template_image:
|
||||||
for asset_data in asset_info['assets']:
|
for asset_data in asset_info["assets"]:
|
||||||
asset_data = dict(asset_data, filepath=asset_path)
|
asset_data = dict(asset_data, filepath=asset_path)
|
||||||
name = asset_data['name']
|
name = asset_data["name"]
|
||||||
dst_asset_path = self.get_asset_bundle_path(asset_data)
|
dst_asset_path = self.get_asset_bundle_path(asset_data)
|
||||||
|
|
||||||
dst_image_path = self.format_path(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():
|
if dst_image_path.exists():
|
||||||
print(f'The dest image {dst_image_path} already exist')
|
print(f"The dest image {dst_image_path} already exist")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Check if a source image exists and if so copying it in the new directory
|
# Check if a source image exists and if so copying it in the new directory
|
||||||
if self.source_template_image:
|
if self.source_template_image:
|
||||||
src_image_path = self.find_path(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:
|
||||||
if src_image_path.suffix == dst_image_path.suffix:
|
if src_image_path.suffix == dst_image_path.suffix:
|
||||||
@@ -170,47 +176,50 @@ class Conform(ScanFolder):
|
|||||||
|
|
||||||
continue
|
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)
|
asset_data_names[name] = dict(asset_data, image_path=dst_image_path)
|
||||||
|
|
||||||
|
if not asset_data_names: # No preview to generate
|
||||||
if not asset_data_names:# No preview to generate
|
|
||||||
return
|
return
|
||||||
|
|
||||||
print('Making Preview for', list(asset_data_names.keys()))
|
print("Making Preview for", list(asset_data_names.keys()))
|
||||||
|
|
||||||
asset_names = list(asset_data_names.keys())
|
asset_names = list(asset_data_names.keys())
|
||||||
assets = self.load_datablocks(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:
|
for asset in assets:
|
||||||
if not asset:
|
if not asset:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
asset_data = asset_data_names[asset.name]
|
asset_data = asset_data_names[asset.name]
|
||||||
image_path = asset_data['image_path']
|
image_path = asset_data["image_path"]
|
||||||
|
|
||||||
if asset.preview:
|
if asset.preview:
|
||||||
print(f'Writing asset preview to {image_path}')
|
print(f"Writing asset preview to {image_path}")
|
||||||
self.write_preview(asset.preview, image_path)
|
self.write_preview(asset.preview, image_path)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if data_type == 'COLLECTION':
|
if data_type == "COLLECTION":
|
||||||
|
|
||||||
bpy.ops.object.collection_instance_add(name=asset.name)
|
bpy.ops.object.collection_instance_add(name=asset.name)
|
||||||
|
|
||||||
bpy.ops.view3d.camera_to_view_selected()
|
bpy.ops.view3d.camera_to_view_selected()
|
||||||
instance = vl.objects.active
|
instance = vl.objects.active
|
||||||
|
|
||||||
#scn.collection.children.link(asset)
|
# scn.collection.children.link(asset)
|
||||||
|
|
||||||
scn.render.filepath = str(image_path)
|
scn.render.filepath = str(image_path)
|
||||||
|
|
||||||
print(f'Render asset {asset.name} to {image_path}')
|
print(f"Render asset {asset.name} to {image_path}")
|
||||||
bpy.ops.render.render(write_still=True)
|
bpy.ops.render.render(write_still=True)
|
||||||
|
|
||||||
#instance.user_clear()
|
# instance.user_clear()
|
||||||
asset.user_clear()
|
asset.user_clear()
|
||||||
|
|
||||||
bpy.data.objects.remove(instance)
|
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
|
||||||
|
)
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
"""
|
||||||
|
Adapter for making an asset library of all blender file found in a folder
|
||||||
|
"""
|
||||||
|
|
||||||
|
from asset_library.library_types.library_type import LibraryType
|
||||||
|
from asset_library.common.file_utils import copy_dir
|
||||||
|
from bpy.props import StringProperty
|
||||||
|
from os.path import expandvars
|
||||||
|
import bpy
|
||||||
|
|
||||||
|
|
||||||
|
class CopyFolder(LibraryType):
|
||||||
|
"""Copy library folder from a server to a local disk for better performance"""
|
||||||
|
|
||||||
|
name = "Copy Folder"
|
||||||
|
source_directory: StringProperty()
|
||||||
|
|
||||||
|
includes: StringProperty()
|
||||||
|
excludes: StringProperty()
|
||||||
|
|
||||||
|
def bundle(self, cache_diff=None):
|
||||||
|
src = expandvars(self.source_directory)
|
||||||
|
dst = expandvars(self.bundle_directory)
|
||||||
|
|
||||||
|
includes = [inc.strip() for inc in self.includes.split(",")]
|
||||||
|
excludes = [ex.strip() for ex in self.excludes.split(",")]
|
||||||
|
|
||||||
|
print(f"Copy Folder from {src} to {dst}...")
|
||||||
|
copy_dir(src, dst, only_recent=True, excludes=excludes, includes=includes)
|
||||||
|
|
||||||
|
def filter_prop(self, prop):
|
||||||
|
if prop in ("template_info", "template_video", "template_image", "blend_depth"):
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
# def draw_prop(self, layout, prop):
|
||||||
|
# if prop in ('template_info', 'template_video', 'template_image', 'blend_depth'):
|
||||||
|
# return
|
||||||
|
|
||||||
|
# super().draw_prop(layout)
|
||||||
@@ -0,0 +1,284 @@
|
|||||||
|
"""
|
||||||
|
Plugin for making an asset library of all blender file found in a folder
|
||||||
|
"""
|
||||||
|
|
||||||
|
from asset_library.library_types.library_type import LibraryType
|
||||||
|
from asset_library.common.template import Template
|
||||||
|
from asset_library.common.file_utils import install_module
|
||||||
|
|
||||||
|
import bpy
|
||||||
|
from bpy.props import StringProperty, IntProperty, BoolProperty
|
||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
from itertools import groupby
|
||||||
|
import uuid
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import json
|
||||||
|
import urllib3
|
||||||
|
import traceback
|
||||||
|
import time
|
||||||
|
|
||||||
|
|
||||||
|
class Kitsu(LibraryType):
|
||||||
|
|
||||||
|
name = "Kitsu"
|
||||||
|
template_name: StringProperty()
|
||||||
|
template_file: StringProperty()
|
||||||
|
source_directory: StringProperty(subtype="DIR_PATH")
|
||||||
|
# blend_depth: IntProperty(default=1)
|
||||||
|
source_template_image: StringProperty()
|
||||||
|
target_template_image: StringProperty()
|
||||||
|
|
||||||
|
url: StringProperty()
|
||||||
|
login: StringProperty()
|
||||||
|
password: StringProperty(subtype="PASSWORD")
|
||||||
|
project_name: StringProperty()
|
||||||
|
|
||||||
|
def connect(self, url=None, login=None, password=None):
|
||||||
|
"""Connect to kitsu api using provided url, login and password"""
|
||||||
|
|
||||||
|
gazu = install_module("gazu")
|
||||||
|
urllib3.disable_warnings()
|
||||||
|
|
||||||
|
if not self.url:
|
||||||
|
print(f"Kitsu Url: {self.url} is empty")
|
||||||
|
return
|
||||||
|
|
||||||
|
url = self.url
|
||||||
|
if not url.endswith("/api"):
|
||||||
|
url += "/api"
|
||||||
|
|
||||||
|
print(f"Info: Setting Host for kitsu {url}")
|
||||||
|
gazu.client.set_host(url)
|
||||||
|
|
||||||
|
if not gazu.client.host_is_up():
|
||||||
|
print("Error: Kitsu Host is down")
|
||||||
|
|
||||||
|
try:
|
||||||
|
print(f"Info: Log in to kitsu as {self.login}")
|
||||||
|
res = gazu.log_in(self.login, self.password)
|
||||||
|
print(f'Info: Sucessfully login to Kitsu as {res["user"]["full_name"]}')
|
||||||
|
return res["user"]
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error: {traceback.format_exc()}")
|
||||||
|
|
||||||
|
def get_asset_path(self, name, catalog, directory=None):
|
||||||
|
directory = directory or self.source_directory
|
||||||
|
return Path(directory, self.get_asset_relative_path(name, catalog))
|
||||||
|
|
||||||
|
def get_asset_info(self, data, asset_path):
|
||||||
|
|
||||||
|
modified = time.time_ns()
|
||||||
|
catalog = data["entity_type_name"].title()
|
||||||
|
asset_path = self.prop_rel_path(asset_path, "source_directory")
|
||||||
|
# asset_name = self.norm_file_name(data['name'])
|
||||||
|
|
||||||
|
asset_info = dict(
|
||||||
|
filepath=asset_path,
|
||||||
|
modified=modified,
|
||||||
|
library_id=self.library.id,
|
||||||
|
assets=[
|
||||||
|
dict(
|
||||||
|
catalog=catalog,
|
||||||
|
metadata=data.get("data", {}),
|
||||||
|
description=data["description"],
|
||||||
|
tags=[],
|
||||||
|
type=self.data_type,
|
||||||
|
# image=self.library.template_image,
|
||||||
|
# video=self.library.template_video,
|
||||||
|
name=data["name"],
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
return asset_info
|
||||||
|
|
||||||
|
# def bundle(self, cache_diff=None):
|
||||||
|
# """Group all asset in one or multiple blends for the asset browser"""
|
||||||
|
|
||||||
|
# return super().bundle(cache_diff=cache_diff)
|
||||||
|
|
||||||
|
def set_asset_preview(self, asset, asset_data):
|
||||||
|
"""Load an externalize image as preview for an asset using the source template"""
|
||||||
|
|
||||||
|
asset_path = self.format_path(Path(asset_data["filepath"]).as_posix())
|
||||||
|
|
||||||
|
image_path = self.find_path(
|
||||||
|
self.target_template_image, asset_data, filepath=asset_path
|
||||||
|
)
|
||||||
|
|
||||||
|
if image_path:
|
||||||
|
with bpy.context.temp_override(id=asset):
|
||||||
|
bpy.ops.ed.lib_id_load_custom_preview(filepath=str(image_path))
|
||||||
|
else:
|
||||||
|
print(f"No image found for {self.target_template_image} on {asset.name}")
|
||||||
|
|
||||||
|
if asset.preview:
|
||||||
|
return asset.preview
|
||||||
|
|
||||||
|
def generate_previews(self, cache=None):
|
||||||
|
|
||||||
|
print("Generate previews...")
|
||||||
|
|
||||||
|
if cache in (None, ""):
|
||||||
|
cache = self.fetch()
|
||||||
|
elif isinstance(cache, (Path, str)):
|
||||||
|
cache = self.read_cache(cache)
|
||||||
|
|
||||||
|
# TODO Support all multiple data_type
|
||||||
|
for asset_info in cache:
|
||||||
|
|
||||||
|
if asset_info.get("type", self.data_type) == "FILE":
|
||||||
|
self.generate_blend_preview(asset_info)
|
||||||
|
else:
|
||||||
|
self.generate_asset_preview(asset_info)
|
||||||
|
|
||||||
|
def generate_asset_preview(self, asset_info):
|
||||||
|
|
||||||
|
data_type = self.data_type
|
||||||
|
scn = bpy.context.scene
|
||||||
|
vl = bpy.context.view_layer
|
||||||
|
|
||||||
|
asset_path = self.format_path(asset_info["filepath"])
|
||||||
|
|
||||||
|
lens = 85
|
||||||
|
|
||||||
|
if not asset_path.exists():
|
||||||
|
print(f"Blend file {asset_path} not exit")
|
||||||
|
return
|
||||||
|
|
||||||
|
asset_data_names = {}
|
||||||
|
|
||||||
|
# First check wich assets need a preview
|
||||||
|
for asset_data in asset_info["assets"]:
|
||||||
|
name = asset_data["name"]
|
||||||
|
image_path = self.format_path(
|
||||||
|
self.target_template_image, asset_data, filepath=asset_path
|
||||||
|
)
|
||||||
|
|
||||||
|
if image_path.exists():
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Store in a dict all asset_data that does not have preview
|
||||||
|
asset_data_names[name] = dict(asset_data, image_path=image_path)
|
||||||
|
|
||||||
|
if not asset_data_names:
|
||||||
|
print(f"All previews already existing for {asset_path}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# asset_names = [a['name'] for a in asset_info['assets']]
|
||||||
|
asset_names = list(asset_data_names.keys())
|
||||||
|
assets = self.load_datablocks(
|
||||||
|
asset_path, names=asset_names, link=True, type=data_type
|
||||||
|
)
|
||||||
|
|
||||||
|
print(asset_names)
|
||||||
|
print(assets)
|
||||||
|
|
||||||
|
for asset in assets:
|
||||||
|
if not asset:
|
||||||
|
continue
|
||||||
|
|
||||||
|
print(f"Generate Preview for asset {asset.name}")
|
||||||
|
|
||||||
|
asset_data = asset_data_names[asset.name]
|
||||||
|
|
||||||
|
# print(self.target_template_image, asset_path)
|
||||||
|
image_path = self.format_path(
|
||||||
|
self.target_template_image, asset_data, filepath=asset_path
|
||||||
|
)
|
||||||
|
|
||||||
|
# Force redo preview
|
||||||
|
# if asset.preview:
|
||||||
|
# print(f'Writing asset preview to {image_path}')
|
||||||
|
# self.write_preview(asset.preview, image_path)
|
||||||
|
# continue
|
||||||
|
|
||||||
|
if data_type == "COLLECTION":
|
||||||
|
|
||||||
|
bpy.ops.object.collection_instance_add(name=asset.name)
|
||||||
|
|
||||||
|
scn.camera.data.lens = lens
|
||||||
|
bpy.ops.view3d.camera_to_view_selected()
|
||||||
|
scn.camera.data.lens -= 5
|
||||||
|
|
||||||
|
instance = vl.objects.active
|
||||||
|
|
||||||
|
# scn.collection.children.link(asset)
|
||||||
|
|
||||||
|
scn.render.filepath = str(image_path)
|
||||||
|
scn.render.image_settings.file_format = self.format_from_ext(
|
||||||
|
image_path.suffix
|
||||||
|
)
|
||||||
|
scn.render.image_settings.color_mode = "RGBA"
|
||||||
|
scn.render.image_settings.quality = 90
|
||||||
|
|
||||||
|
print(f"Render asset {asset.name} to {image_path}")
|
||||||
|
bpy.ops.render.render(write_still=True)
|
||||||
|
|
||||||
|
# instance.user_clear()
|
||||||
|
asset.user_clear()
|
||||||
|
|
||||||
|
bpy.data.objects.remove(instance)
|
||||||
|
|
||||||
|
bpy.ops.outliner.orphans_purge(
|
||||||
|
do_local_ids=True, do_linked_ids=True, do_recursive=True
|
||||||
|
)
|
||||||
|
|
||||||
|
def fetch(self):
|
||||||
|
"""Gather in a list all assets found in the folder"""
|
||||||
|
|
||||||
|
print(f"Fetch Assets for {self.library.name}")
|
||||||
|
|
||||||
|
gazu = install_module("gazu")
|
||||||
|
self.connect()
|
||||||
|
|
||||||
|
template_file = Template(self.template_file)
|
||||||
|
template_name = Template(self.template_name)
|
||||||
|
|
||||||
|
project = gazu.client.fetch_first("projects", {"name": self.project_name})
|
||||||
|
entity_types = gazu.client.fetch_all("entity-types")
|
||||||
|
entity_types_ids = {e["id"]: e["name"] for e in entity_types}
|
||||||
|
|
||||||
|
cache = self.read_cache()
|
||||||
|
|
||||||
|
for asset_data in gazu.asset.all_assets_for_project(project):
|
||||||
|
asset_data["entity_type_name"] = entity_types_ids[
|
||||||
|
asset_data.pop("entity_type_id")
|
||||||
|
]
|
||||||
|
asset_name = asset_data["name"]
|
||||||
|
|
||||||
|
asset_field_data = dict(
|
||||||
|
asset_name=asset_name,
|
||||||
|
type=asset_data["entity_type_name"],
|
||||||
|
source_directory=self.source_directory,
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
asset_field_data.update(template_name.parse(asset_name))
|
||||||
|
except Exception:
|
||||||
|
print(
|
||||||
|
f"Warning: Could not parse {asset_name} with template {template_name}"
|
||||||
|
)
|
||||||
|
|
||||||
|
asset_path = template_file.find(asset_field_data)
|
||||||
|
if not asset_path:
|
||||||
|
print(
|
||||||
|
f"Warning: Could not find file for {template_file.format(asset_field_data)}"
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
asset_path = self.prop_rel_path(asset_path, "source_directory")
|
||||||
|
asset_cache_data = dict(
|
||||||
|
catalog=asset_data["entity_type_name"].title(),
|
||||||
|
metadata=asset_data.get("data", {}),
|
||||||
|
description=asset_data["description"],
|
||||||
|
tags=[],
|
||||||
|
type=self.data_type,
|
||||||
|
name=asset_data["name"],
|
||||||
|
)
|
||||||
|
|
||||||
|
cache.add_asset_cache(asset_cache_data, filepath=asset_path)
|
||||||
|
|
||||||
|
return cache
|
||||||
@@ -1,43 +1,41 @@
|
|||||||
|
# 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
|
||||||
|
|
||||||
import os
|
from asset_library import action, collection, file
|
||||||
|
from asset_library.common.library_cache import LibraryCacheDiff
|
||||||
|
|
||||||
|
from bpy.types import PropertyGroup
|
||||||
|
from bpy.props import StringProperty
|
||||||
|
import bpy
|
||||||
|
from bpy_extras import asset_utils
|
||||||
|
|
||||||
|
from itertools import groupby
|
||||||
|
from pathlib import Path
|
||||||
import shutil
|
import shutil
|
||||||
|
import os
|
||||||
import json
|
import json
|
||||||
import uuid
|
import uuid
|
||||||
import time
|
import time
|
||||||
import subprocess
|
|
||||||
from pathlib import Path
|
|
||||||
from itertools import groupby
|
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
import subprocess
|
||||||
from glob import glob
|
from glob import glob
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
|
|
||||||
import bpy
|
|
||||||
from bpy_extras import asset_utils
|
|
||||||
from bpy.types import PropertyGroup
|
|
||||||
from bpy.props import StringProperty
|
|
||||||
|
|
||||||
#from asset_library.common.functions import (norm_asset_datas,)
|
class LibraryType(PropertyGroup):
|
||||||
from asset_library.core.bl_utils import get_addon_prefs, load_datablocks
|
|
||||||
from asset_library.core.file_utils import read_file, write_file
|
|
||||||
from asset_library.core.template import Template
|
|
||||||
from asset_library.constants import (MODULE_DIR, RESOURCES_DIR)
|
|
||||||
|
|
||||||
#from asset_library.data_type import (action, collection, file)
|
# def __init__(self):
|
||||||
#from asset_library.common.library_cache import LibraryCacheDiff
|
name = "Base Adapter"
|
||||||
|
# library = None
|
||||||
|
|
||||||
|
|
||||||
class LibraryPlugin(PropertyGroup):
|
|
||||||
|
|
||||||
#def __init__(self):
|
|
||||||
#name = "Base Adapter"
|
|
||||||
#library = None
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def library(self):
|
def library(self):
|
||||||
prefs = self.addon_prefs
|
prefs = self.addon_prefs
|
||||||
for lib in prefs.libraries:
|
for lib in prefs.libraries:
|
||||||
if lib.plugin == self:
|
if lib.library_type == self:
|
||||||
return lib
|
return lib
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -83,20 +81,28 @@ class LibraryPlugin(PropertyGroup):
|
|||||||
@property
|
@property
|
||||||
def module_type(self):
|
def module_type(self):
|
||||||
lib_type = self.library.data_type
|
lib_type = self.library.data_type
|
||||||
if lib_type == 'ACTION':
|
if lib_type == "ACTION":
|
||||||
return action
|
return action
|
||||||
elif lib_type == 'FILE':
|
elif lib_type == "FILE":
|
||||||
return file
|
return file
|
||||||
elif lib_type == 'COLLECTION':
|
elif lib_type == "COLLECTION":
|
||||||
return collection
|
return collection
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def format_data(self):
|
def format_data(self):
|
||||||
"""Dict for formating template"""
|
"""Dict for formating template"""
|
||||||
return dict(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):
|
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):
|
def read_catalog(self):
|
||||||
return self.library.read_catalog()
|
return self.library.read_catalog()
|
||||||
@@ -105,10 +111,10 @@ class LibraryPlugin(PropertyGroup):
|
|||||||
return self.library.read_cache(filepath=filepath)
|
return self.library.read_cache(filepath=filepath)
|
||||||
|
|
||||||
def fetch(self):
|
def fetch(self):
|
||||||
raise Exception('This method need to be define in the plugin')
|
raise Exception("This method need to be define in the library_type")
|
||||||
|
|
||||||
def norm_file_name(self, name):
|
def norm_file_name(self, name):
|
||||||
return name.replace(' ', '_')
|
return name.replace(" ", "_")
|
||||||
|
|
||||||
def read_file(self, file):
|
def read_file(self, file):
|
||||||
return read_file(file)
|
return read_file(file)
|
||||||
@@ -121,42 +127,46 @@ class LibraryPlugin(PropertyGroup):
|
|||||||
dst = Path(destination)
|
dst = Path(destination)
|
||||||
|
|
||||||
if not src.exists():
|
if not src.exists():
|
||||||
print(f'Cannot copy file {src}: file not exist')
|
print(f"Cannot copy file {src}: file not exist")
|
||||||
return
|
return
|
||||||
|
|
||||||
dst.parent.mkdir(exist_ok=True, parents=True)
|
dst.parent.mkdir(exist_ok=True, parents=True)
|
||||||
|
|
||||||
if src == dst:
|
if src == dst:
|
||||||
print(f'Cannot copy file {src}: source and destination are the same')
|
print(f"Cannot copy file {src}: source and destination are the same")
|
||||||
return
|
return
|
||||||
|
|
||||||
print(f'Copy file from {src} to {dst}')
|
print(f"Copy file from {src} to {dst}")
|
||||||
shutil.copy2(str(src), str(dst))
|
shutil.copy2(str(src), str(dst))
|
||||||
|
|
||||||
def load_datablocks(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"""
|
"""Link or append a datablock from a blendfile"""
|
||||||
|
|
||||||
if type.isupper():
|
if type.isupper():
|
||||||
type = f'{type.lower()}s'
|
type = f"{type.lower()}s"
|
||||||
|
|
||||||
return load_datablocks(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):
|
def get_asset_data(self, asset):
|
||||||
"""Extract asset information on a datablock"""
|
"""Extract asset information on a datablock"""
|
||||||
|
|
||||||
return dict(
|
return dict(
|
||||||
name=asset.name,
|
name=asset.name,
|
||||||
type=asset.bl_rna.name.upper(),
|
type=asset.bl_rna.name.upper(),
|
||||||
author=asset.asset_data.author,
|
author=asset.asset_data.author,
|
||||||
tags=list(asset.asset_data.tags.keys()),
|
tags=list(asset.asset_data.tags.keys()),
|
||||||
metadata=dict(asset.asset_data),
|
metadata=dict(asset.asset_data),
|
||||||
description=asset.asset_data.description,
|
description=asset.asset_data.description,
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_asset_relative_path(self, name, catalog):
|
def get_asset_relative_path(self, name, catalog):
|
||||||
'''Get a relative path for the asset'''
|
"""Get a relative path for the asset"""
|
||||||
name = self.norm_file_name(name)
|
name = self.norm_file_name(name)
|
||||||
return Path(catalog, name, name).with_suffix('.blend')
|
return Path(catalog, name, name).with_suffix(".blend")
|
||||||
|
|
||||||
def get_active_asset_library(self):
|
def get_active_asset_library(self):
|
||||||
prefs = get_addon_prefs()
|
prefs = get_addon_prefs()
|
||||||
@@ -166,8 +176,8 @@ class LibraryPlugin(PropertyGroup):
|
|||||||
return self
|
return self
|
||||||
|
|
||||||
lib = None
|
lib = None
|
||||||
if '.library_id' in asset_handle.asset_data:
|
if ".library_id" in asset_handle.asset_data:
|
||||||
lib_id = asset_handle.asset_data['.library_id']
|
lib_id = asset_handle.asset_data[".library_id"]
|
||||||
lib = next((l for l in prefs.libraries if l.id == lib_id), None)
|
lib = next((l for l in prefs.libraries if l.id == lib_id), None)
|
||||||
|
|
||||||
if not lib:
|
if not lib:
|
||||||
@@ -179,15 +189,15 @@ class LibraryPlugin(PropertyGroup):
|
|||||||
return lib
|
return lib
|
||||||
|
|
||||||
def get_active_asset_path(self):
|
def get_active_asset_path(self):
|
||||||
'''Get the full path of the active asset_handle from the asset brower'''
|
"""Get the full path of the active asset_handle from the asset brower"""
|
||||||
prefs = get_addon_prefs()
|
prefs = get_addon_prefs()
|
||||||
asset_handle = bpy.context.asset_file_handle
|
asset_handle = bpy.context.asset_file_handle
|
||||||
|
|
||||||
lib = self.get_active_asset_library()
|
lib = self.get_active_asset_library()
|
||||||
|
|
||||||
if 'filepath' in asset_handle.asset_data:
|
if "filepath" in asset_handle.asset_data:
|
||||||
asset_path = asset_handle.asset_data['filepath']
|
asset_path = asset_handle.asset_data["filepath"]
|
||||||
asset_path = lib.plugin.format_path(asset_path)
|
asset_path = lib.library_type.format_path(asset_path)
|
||||||
else:
|
else:
|
||||||
asset_path = bpy.types.AssetHandle.get_full_library_path(
|
asset_path = bpy.types.AssetHandle.get_full_library_path(
|
||||||
asset_handle, bpy.context.asset_library_ref
|
asset_handle, bpy.context.asset_library_ref
|
||||||
@@ -196,30 +206,30 @@ class LibraryPlugin(PropertyGroup):
|
|||||||
return asset_path
|
return asset_path
|
||||||
|
|
||||||
def generate_previews(self):
|
def generate_previews(self):
|
||||||
raise Exception('Need to be defined in the plugin')
|
raise Exception("Need to be defined in the library_type")
|
||||||
|
|
||||||
def get_image_path(self, name, catalog, filepath):
|
def get_image_path(self, name, catalog, filepath):
|
||||||
raise Exception('Need to be defined in the plugin')
|
raise Exception("Need to be defined in the library_type")
|
||||||
|
|
||||||
def get_video_path(self, name, catalog, filepath):
|
def get_video_path(self, name, catalog, filepath):
|
||||||
raise Exception('Need to be defined in the plugin')
|
raise Exception("Need to be defined in the library_type")
|
||||||
|
|
||||||
def new_asset(self, asset, asset_cache):
|
def new_asset(self, asset, asset_cache):
|
||||||
raise Exception('Need to be defined in the plugin')
|
raise Exception("Need to be defined in the library_type")
|
||||||
|
|
||||||
def remove_asset(self, asset, asset_cache):
|
def remove_asset(self, asset, asset_cache):
|
||||||
raise Exception('Need to be defined in the plugin')
|
raise Exception("Need to be defined in the library_type")
|
||||||
|
|
||||||
def set_asset_preview(self, asset, asset_cache):
|
def set_asset_preview(self, asset, asset_cache):
|
||||||
raise Exception('Need to be defined in the plugin')
|
raise Exception("Need to be defined in the library_type")
|
||||||
|
|
||||||
def format_asset_data(self, data):
|
def format_asset_data(self, data):
|
||||||
"""Get a dict for use in template fields"""
|
"""Get a dict for use in template fields"""
|
||||||
return {
|
return {
|
||||||
'asset_name': data['name'],
|
"asset_name": data["name"],
|
||||||
'asset_path': Path(data['filepath']),
|
"asset_path": Path(data["filepath"]),
|
||||||
'catalog': data['catalog'],
|
"catalog": data["catalog"],
|
||||||
'catalog_name': data['catalog'].replace('/', '_'),
|
"catalog_name": data["catalog"].replace("/", "_"),
|
||||||
}
|
}
|
||||||
|
|
||||||
def format_path(self, template, data={}, **kargs):
|
def format_path(self, template, data={}, **kargs):
|
||||||
@@ -231,8 +241,8 @@ class LibraryPlugin(PropertyGroup):
|
|||||||
else:
|
else:
|
||||||
data = kargs
|
data = kargs
|
||||||
|
|
||||||
if template.startswith('.'): #the template is relative
|
if template.startswith("."): # the template is relative
|
||||||
template = Path(data['asset_path'], template).as_posix()
|
template = Path(data["asset_path"], template).as_posix()
|
||||||
|
|
||||||
params = dict(
|
params = dict(
|
||||||
**data,
|
**data,
|
||||||
@@ -262,11 +272,7 @@ class LibraryPlugin(PropertyGroup):
|
|||||||
Path(asset_path).parent.mkdir(exist_ok=True, parents=True)
|
Path(asset_path).parent.mkdir(exist_ok=True, parents=True)
|
||||||
|
|
||||||
bpy.data.libraries.write(
|
bpy.data.libraries.write(
|
||||||
str(asset_path),
|
str(asset_path), {asset}, path_remap="NONE", fake_user=True, compress=True
|
||||||
{asset},
|
|
||||||
path_remap="NONE",
|
|
||||||
fake_user=True,
|
|
||||||
compress=True
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# def read_catalog(self, directory=None):
|
# def read_catalog(self, directory=None):
|
||||||
@@ -322,8 +328,8 @@ class LibraryPlugin(PropertyGroup):
|
|||||||
# return write_file(cache_path, list(asset_infos))
|
# return write_file(cache_path, list(asset_infos))
|
||||||
|
|
||||||
def prop_rel_path(self, path, prop):
|
def prop_rel_path(self, path, prop):
|
||||||
'''Get a filepath relative to a property of the plugin'''
|
"""Get a filepath relative to a property of the library_type"""
|
||||||
field_prop = '{%s}/'%prop
|
field_prop = "{%s}/" % prop
|
||||||
|
|
||||||
prop_value = getattr(self, prop)
|
prop_value = getattr(self, prop)
|
||||||
prop_value = Path(os.path.expandvars(prop_value)).resolve()
|
prop_value = Path(os.path.expandvars(prop_value)).resolve()
|
||||||
@@ -333,15 +339,15 @@ class LibraryPlugin(PropertyGroup):
|
|||||||
return field_prop + rel_path
|
return field_prop + rel_path
|
||||||
|
|
||||||
def format_from_ext(self, ext):
|
def format_from_ext(self, ext):
|
||||||
if ext.startswith('.'):
|
if ext.startswith("."):
|
||||||
ext = ext[1:]
|
ext = ext[1:]
|
||||||
|
|
||||||
file_format = ext.upper()
|
file_format = ext.upper()
|
||||||
|
|
||||||
if file_format == 'JPG':
|
if file_format == "JPG":
|
||||||
file_format = 'JPEG'
|
file_format = "JPEG"
|
||||||
elif file_format == 'EXR':
|
elif file_format == "EXR":
|
||||||
file_format = 'OPEN_EXR'
|
file_format = "OPEN_EXR"
|
||||||
|
|
||||||
return file_format
|
return file_format
|
||||||
|
|
||||||
@@ -374,15 +380,20 @@ class LibraryPlugin(PropertyGroup):
|
|||||||
|
|
||||||
px = [0] * img_size[0] * img_size[1] * 4
|
px = [0] * img_size[0] * img_size[1] * 4
|
||||||
preview.image_pixels_float.foreach_get(px)
|
preview.image_pixels_float.foreach_get(px)
|
||||||
img = bpy.data.images.new(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)
|
img.pixels.foreach_set(px)
|
||||||
|
|
||||||
self.save_image(img, filepath, remove=True)
|
self.save_image(img, filepath, remove=True)
|
||||||
|
|
||||||
|
|
||||||
def draw_header(self, layout):
|
def draw_header(self, layout):
|
||||||
"""Draw the header of the Asset Browser Window"""
|
"""Draw the header of the Asset Browser Window"""
|
||||||
#layout.separator()
|
# layout.separator()
|
||||||
|
|
||||||
self.module_type.gui.draw_header(layout)
|
self.module_type.gui.draw_header(layout)
|
||||||
|
|
||||||
@@ -391,25 +402,27 @@ class LibraryPlugin(PropertyGroup):
|
|||||||
self.module_type.gui.draw_context_menu(layout)
|
self.module_type.gui.draw_context_menu(layout)
|
||||||
|
|
||||||
def generate_blend_preview(self, asset_info):
|
def generate_blend_preview(self, asset_info):
|
||||||
asset_name = asset_info['name']
|
asset_name = asset_info["name"]
|
||||||
catalog = asset_info['catalog']
|
catalog = asset_info["catalog"]
|
||||||
|
|
||||||
asset_path = self.format_path(asset_info['filepath'])
|
asset_path = self.format_path(asset_info["filepath"])
|
||||||
dst_image_path = self.get_image_path(asset_name, asset_path, catalog)
|
dst_image_path = self.get_image_path(asset_name, asset_path, catalog)
|
||||||
|
|
||||||
if dst_image_path.exists():
|
if dst_image_path.exists():
|
||||||
return
|
return
|
||||||
|
|
||||||
# Check if a source image exists and if so copying it in the new directory
|
# Check if a source image exists and if so copying it in the new directory
|
||||||
src_image_path = asset_info.get('image')
|
src_image_path = asset_info.get("image")
|
||||||
if src_image_path:
|
if src_image_path:
|
||||||
src_image_path = self.get_template_path(src_image_path, 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():
|
if src_image_path and src_image_path.exists():
|
||||||
self.copy_file(src_image_path, dst_image_path)
|
self.copy_file(src_image_path, dst_image_path)
|
||||||
return
|
return
|
||||||
|
|
||||||
print(f'Thumbnailing {asset_path} to {dst_image_path}')
|
print(f"Thumbnailing {asset_path} to {dst_image_path}")
|
||||||
blender_thumbnailer = Path(bpy.app.binary_path).parent / 'blender-thumbnailer'
|
blender_thumbnailer = Path(bpy.app.binary_path).parent / "blender-thumbnailer"
|
||||||
|
|
||||||
dst_image_path.parent.mkdir(exist_ok=True, parents=True)
|
dst_image_path.parent.mkdir(exist_ok=True, parents=True)
|
||||||
|
|
||||||
@@ -418,7 +431,7 @@ class LibraryPlugin(PropertyGroup):
|
|||||||
success = dst_image_path.exists()
|
success = dst_image_path.exists()
|
||||||
|
|
||||||
if not success:
|
if not success:
|
||||||
empty_preview = RESOURCES_DIR / 'empty_preview.png'
|
empty_preview = RESOURCES_DIR / "empty_preview.png"
|
||||||
self.copy_file(str(empty_preview), str(dst_image_path))
|
self.copy_file(str(empty_preview), str(dst_image_path))
|
||||||
|
|
||||||
return success
|
return success
|
||||||
@@ -533,14 +546,12 @@ class LibraryPlugin(PropertyGroup):
|
|||||||
# def set_asset_catalog(self, asset, asset_data, catalog_data):
|
# def set_asset_catalog(self, asset, asset_data, catalog_data):
|
||||||
# """Find the catalog if already exist or create it"""
|
# """Find the catalog if already exist or create it"""
|
||||||
|
|
||||||
|
|
||||||
# catalog_name = asset_data['catalog']
|
# catalog_name = asset_data['catalog']
|
||||||
# catalog = catalog_data.get(catalog_name)
|
# catalog = catalog_data.get(catalog_name)
|
||||||
|
|
||||||
# catalog_item = self.catalog.add(asset_data['catalog'])
|
# catalog_item = self.catalog.add(asset_data['catalog'])
|
||||||
# asset.asset_data.catalog_id = catalog_item.id
|
# asset.asset_data.catalog_id = catalog_item.id
|
||||||
|
|
||||||
|
|
||||||
# if not catalog:
|
# if not catalog:
|
||||||
# catalog = {'id': str(uuid.uuid4()), 'name': catalog_name}
|
# catalog = {'id': str(uuid.uuid4()), 'name': catalog_name}
|
||||||
# catalog_data[catalog_name] = catalog
|
# catalog_data[catalog_name] = catalog
|
||||||
@@ -571,18 +582,22 @@ class LibraryPlugin(PropertyGroup):
|
|||||||
"""Get the bundle path for that asset"""
|
"""Get the bundle path for that asset"""
|
||||||
catalog_parts = asset_cache.catalog_item.parts
|
catalog_parts = asset_cache.catalog_item.parts
|
||||||
blend_name = asset_cache.norm_name
|
blend_name = asset_cache.norm_name
|
||||||
path_parts = catalog_parts[:self.library.blend_depth]
|
path_parts = catalog_parts[: self.library.blend_depth]
|
||||||
|
|
||||||
return Path(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):
|
def bundle(self, cache_diff=None):
|
||||||
"""Group all new assets in one or multiple blends for the asset browser"""
|
"""Group all new assets in one or multiple blends for the asset browser"""
|
||||||
|
|
||||||
supported_types = ('FILE', 'ACTION', 'COLLECTION')
|
supported_types = ("FILE", "ACTION", "COLLECTION")
|
||||||
supported_operations = ('ADD', 'REMOVE', 'MODIFY')
|
supported_operations = ("ADD", "REMOVE", "MODIFY")
|
||||||
|
|
||||||
if self.data_type not in supported_types:
|
if self.data_type not in supported_types:
|
||||||
print(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
|
return
|
||||||
|
|
||||||
catalog = self.read_catalog()
|
catalog = self.read_catalog()
|
||||||
@@ -596,66 +611,75 @@ class LibraryPlugin(PropertyGroup):
|
|||||||
|
|
||||||
# Write the cache in a temporary file for the generate preview script
|
# Write the cache in a temporary file for the generate preview script
|
||||||
tmp_cache_file = cache.write(tmp=True)
|
tmp_cache_file = cache.write(tmp=True)
|
||||||
bpy.ops.assetlibrary.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)):
|
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)
|
total_diffs = len(cache_diff)
|
||||||
print(f'Total Diffs={total_diffs}')
|
print(f"Total Diffs={total_diffs}")
|
||||||
|
|
||||||
if total_diffs == 0:
|
if total_diffs == 0:
|
||||||
print('No assets found')
|
print("No assets found")
|
||||||
return
|
return
|
||||||
|
|
||||||
i = 0
|
i = 0
|
||||||
for bundle_path, asset_diffs in cache_diff.group_by(self.get_asset_bundle_path):
|
for bundle_path, asset_diffs in cache_diff.group_by(self.get_asset_bundle_path):
|
||||||
if bundle_path.exists():
|
if bundle_path.exists():
|
||||||
print(f'Opening existing bundle blend: {bundle_path}')
|
print(f"Opening existing bundle blend: {bundle_path}")
|
||||||
bpy.ops.wm.open_mainfile(filepath=str(bundle_path))
|
bpy.ops.wm.open_mainfile(filepath=str(bundle_path))
|
||||||
else:
|
else:
|
||||||
print(f'Create new bundle blend to: {bundle_path}')
|
print(f"Create new bundle blend to: {bundle_path}")
|
||||||
bpy.ops.wm.read_homefile(use_empty=True)
|
bpy.ops.wm.read_homefile(use_empty=True)
|
||||||
|
|
||||||
for asset_diff in asset_diffs:
|
for asset_diff in asset_diffs:
|
||||||
if total_diffs <= 100 or i % int(total_diffs / 10) == 0:
|
if total_diffs <= 100 or i % int(total_diffs / 10) == 0:
|
||||||
print(f'Progress: {int(i / total_diffs * 100)+1}')
|
print(f"Progress: {int(i / total_diffs * 100)+1}")
|
||||||
|
|
||||||
operation = asset_diff.operation
|
operation = asset_diff.operation
|
||||||
asset_cache = asset_diff.asset_cache
|
asset_cache = asset_diff.asset_cache
|
||||||
asset = getattr(bpy.data, self.data_types).get(asset_cache.name)
|
asset = getattr(bpy.data, self.data_types).get(asset_cache.name)
|
||||||
|
|
||||||
if operation == 'REMOVE':
|
if operation == "REMOVE":
|
||||||
if asset:
|
if asset:
|
||||||
getattr(bpy.data, self.data_types).remove(asset)
|
getattr(bpy.data, self.data_types).remove(asset)
|
||||||
else:
|
else:
|
||||||
print(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
|
continue
|
||||||
|
|
||||||
elif operation == 'MODIFY':
|
elif operation == "MODIFY":
|
||||||
if not asset:
|
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:
|
if asset:
|
||||||
#raise Exception(f"Asset {asset_data['name']} Already in Blend")
|
# raise Exception(f"Asset {asset_data['name']} Already in Blend")
|
||||||
print(f"Asset {asset_cache.name} Already in Blend")
|
print(f"Asset {asset_cache.name} Already in Blend")
|
||||||
getattr(bpy.data, self.data_types).remove(asset)
|
getattr(bpy.data, self.data_types).remove(asset)
|
||||||
|
|
||||||
#print(f"INFO: Add new asset: {asset_data['name']}")
|
# print(f"INFO: Add new asset: {asset_data['name']}")
|
||||||
asset = getattr(bpy.data, self.data_types).new(name=asset_cache.name)
|
asset = getattr(bpy.data, self.data_types).new(
|
||||||
|
name=asset_cache.name
|
||||||
|
)
|
||||||
|
|
||||||
asset.asset_mark()
|
asset.asset_mark()
|
||||||
|
|
||||||
self.set_asset_preview(asset, asset_cache)
|
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']))
|
# 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.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
|
asset.asset_data.catalog_id = catalog.add(asset_cache.catalog).id
|
||||||
|
|
||||||
@@ -663,12 +687,11 @@ class LibraryPlugin(PropertyGroup):
|
|||||||
self.set_asset_tags(asset, asset_cache)
|
self.set_asset_tags(asset, asset_cache)
|
||||||
self.set_asset_info(asset, asset_cache)
|
self.set_asset_info(asset, asset_cache)
|
||||||
|
|
||||||
|
|
||||||
i += 1
|
i += 1
|
||||||
|
|
||||||
#self.write_asset_preview_file()
|
# self.write_asset_preview_file()
|
||||||
|
|
||||||
print(f'Saving Blend to {bundle_path}')
|
print(f"Saving Blend to {bundle_path}")
|
||||||
|
|
||||||
bundle_path.parent.mkdir(exist_ok=True, parents=True)
|
bundle_path.parent.mkdir(exist_ok=True, parents=True)
|
||||||
bpy.ops.wm.save_as_mainfile(filepath=str(bundle_path), compress=True)
|
bpy.ops.wm.save_as_mainfile(filepath=str(bundle_path), compress=True)
|
||||||
@@ -676,10 +699,9 @@ class LibraryPlugin(PropertyGroup):
|
|||||||
if write_cache:
|
if write_cache:
|
||||||
cache.write()
|
cache.write()
|
||||||
|
|
||||||
#self.write_catalog(catalog_data)
|
# self.write_catalog(catalog_data)
|
||||||
catalog.write()
|
catalog.write()
|
||||||
|
|
||||||
|
|
||||||
bpy.ops.wm.quit_blender()
|
bpy.ops.wm.quit_blender()
|
||||||
|
|
||||||
# def unflatten_cache(self, cache):
|
# def unflatten_cache(self, cache):
|
||||||
@@ -765,9 +787,8 @@ class LibraryPlugin(PropertyGroup):
|
|||||||
# return list(new_cache.values()), cache_diff
|
# return list(new_cache.values()), cache_diff
|
||||||
|
|
||||||
def draw_prefs(self, layout):
|
def draw_prefs(self, layout):
|
||||||
"""Draw the options in the addon preference for this plugin"""
|
"""Draw the options in the addon preference for this library_type"""
|
||||||
|
|
||||||
annotations = self.__class__.__annotations__
|
annotations = self.__class__.__annotations__
|
||||||
for k, v in annotations.items():
|
for k, v in annotations.items():
|
||||||
layout.prop(self, k, text=bpy.path.display_name(k))
|
layout.prop(self, k, text=bpy.path.display_name(k))
|
||||||
|
|
||||||
@@ -1,47 +1,52 @@
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
Plugin for making an asset library of all blender file found in a folder
|
Plugin for making an asset library of all blender file found in a folder
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
from asset_library.library_types.library_type import LibraryType
|
||||||
|
from asset_library.common.template import Template
|
||||||
|
from asset_library.common.file_utils import install_module
|
||||||
|
|
||||||
|
import bpy
|
||||||
|
from bpy.props import StringProperty, IntProperty, BoolProperty, EnumProperty
|
||||||
import re
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
from itertools import groupby
|
||||||
import uuid
|
import uuid
|
||||||
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import json
|
import json
|
||||||
import requests
|
import requests
|
||||||
import urllib3
|
import urllib3
|
||||||
import traceback
|
import traceback
|
||||||
import time
|
import time
|
||||||
from itertools import groupby
|
|
||||||
from pathlib import Path
|
|
||||||
from pprint import pprint as pp
|
from pprint import pprint as pp
|
||||||
|
|
||||||
import bpy
|
|
||||||
from bpy.props import (StringProperty, IntProperty, BoolProperty, EnumProperty)
|
|
||||||
|
|
||||||
from asset_library.plugins.library_plugin import LibraryPlugin
|
|
||||||
from asset_library.core.template import Template
|
|
||||||
from asset_library.core.file_utils import install_module
|
|
||||||
|
|
||||||
|
|
||||||
REQ_HEADERS = requests.utils.default_headers()
|
REQ_HEADERS = requests.utils.default_headers()
|
||||||
REQ_HEADERS.update({"User-Agent": "Blender: PH Assets"})
|
REQ_HEADERS.update({"User-Agent": "Blender: PH Assets"})
|
||||||
|
|
||||||
class PolyHaven(LibraryPlugin):
|
|
||||||
|
class PolyHaven(LibraryType):
|
||||||
|
|
||||||
name = "Poly Haven"
|
name = "Poly Haven"
|
||||||
# template_name : StringProperty()
|
# template_name : StringProperty()
|
||||||
# template_file : StringProperty()
|
# template_file : StringProperty()
|
||||||
directory : StringProperty(subtype='DIR_PATH')
|
directory: StringProperty(subtype="DIR_PATH")
|
||||||
asset_type : EnumProperty(items=[(i.replace(' ', '_').upper(), i, '') for i in ('HDRIs', 'Models', 'Textures')], default='HDRIS')
|
asset_type: EnumProperty(
|
||||||
main_category : StringProperty(
|
items=[
|
||||||
default='artificial light, natural light, nature, studio, skies, urban'
|
(i.replace(" ", "_").upper(), i, "")
|
||||||
|
for i in ("HDRIs", "Models", "Textures")
|
||||||
|
],
|
||||||
|
default="HDRIS",
|
||||||
)
|
)
|
||||||
secondary_category : StringProperty(
|
main_category: StringProperty(
|
||||||
default='high constrast, low constrast, medium constrast, midday, morning-afternoon, night, sunrise-sunset'
|
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()
|
# url: StringProperty()
|
||||||
# login: StringProperty()
|
# login: StringProperty()
|
||||||
@@ -64,32 +69,35 @@ class PolyHaven(LibraryPlugin):
|
|||||||
def format_asset_info(self, asset_info, asset_path):
|
def format_asset_info(self, asset_info, asset_path):
|
||||||
# prend un asset info et output un asset description
|
# prend un asset info et output un asset description
|
||||||
|
|
||||||
asset_path = self.prop_rel_path(asset_path, 'source_directory')
|
asset_path = self.prop_rel_path(asset_path, "source_directory")
|
||||||
modified = asset_info.get('modified', time.time_ns())
|
modified = asset_info.get("modified", time.time_ns())
|
||||||
|
|
||||||
return dict(
|
return dict(
|
||||||
filepath=asset_path,
|
filepath=asset_path,
|
||||||
modified=modified,
|
modified=modified,
|
||||||
library_id=self.library.id,
|
library_id=self.library.id,
|
||||||
assets=[dict(
|
assets=[
|
||||||
catalog=asset_data.get('catalog', asset_info['catalog']),
|
dict(
|
||||||
author=asset_data.get('author'),
|
catalog=asset_data.get("catalog", asset_info["catalog"]),
|
||||||
metadata=asset_data.get('metadata', {}),
|
author=asset_data.get("author"),
|
||||||
description=asset_data.get('description', ''),
|
metadata=asset_data.get("metadata", {}),
|
||||||
tags=asset_data.get('tags', []),
|
description=asset_data.get("description", ""),
|
||||||
type=self.data_type,
|
tags=asset_data.get("tags", []),
|
||||||
image=self.template_image,
|
type=self.data_type,
|
||||||
video=self.template_video,
|
image=self.template_image,
|
||||||
name=asset_data['name']) for asset_data in asset_info['assets']
|
video=self.template_video,
|
||||||
]
|
name=asset_data["name"],
|
||||||
|
)
|
||||||
|
for asset_data in asset_info["assets"]
|
||||||
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
def fetch(self):
|
def fetch(self):
|
||||||
"""Gather in a list all assets found in the folder"""
|
"""Gather in a list all assets found in the folder"""
|
||||||
|
|
||||||
print(f'Fetch Assets for {self.library.name}')
|
print(f"Fetch Assets for {self.library.name}")
|
||||||
|
|
||||||
print('self.asset_type: ', self.asset_type)
|
print("self.asset_type: ", self.asset_type)
|
||||||
url = f"https://api.polyhaven.com/assets?t={self.asset_type.lower()}"
|
url = f"https://api.polyhaven.com/assets?t={self.asset_type.lower()}"
|
||||||
# url2 = f"https://polyhaven.com/{self.asset_type.lower()}"
|
# url2 = f"https://polyhaven.com/{self.asset_type.lower()}"
|
||||||
# url += "&future=true" if early_access else ""
|
# url += "&future=true" if early_access else ""
|
||||||
@@ -115,27 +123,26 @@ class PolyHaven(LibraryPlugin):
|
|||||||
for asset_info in res.json().values():
|
for asset_info in res.json().values():
|
||||||
main_category = None
|
main_category = None
|
||||||
secondary_category = None
|
secondary_category = None
|
||||||
for category in asset_info['categories']:
|
for category in asset_info["categories"]:
|
||||||
if category in self.main_category and not main_category:
|
if category in self.main_category and not main_category:
|
||||||
main_category = category
|
main_category = category
|
||||||
if category in self.secondary_category and not secondary_category:
|
if category in self.secondary_category and not secondary_category:
|
||||||
secondary_category = category
|
secondary_category = category
|
||||||
|
|
||||||
if main_category and secondary_category:
|
if main_category and secondary_category:
|
||||||
catalog = f'{main_category}_{secondary_category}'
|
catalog = f"{main_category}_{secondary_category}"
|
||||||
|
|
||||||
if not catalog:
|
if not catalog:
|
||||||
return
|
return
|
||||||
|
|
||||||
asset_path = self.get_asset_path(asset_info['name'], catalog)
|
asset_path = self.get_asset_path(asset_info["name"], catalog)
|
||||||
print('asset_path: ', asset_path)
|
print("asset_path: ", asset_path)
|
||||||
asset_info = self.format_asset_info(asset_info, asset_path)
|
asset_info = self.format_asset_info(asset_info, asset_path)
|
||||||
print('asset_info: ', asset_info)
|
print("asset_info: ", asset_info)
|
||||||
|
|
||||||
# return self.format_asset_info([asset['name'], self.get_asset_path(asset['name'], catalog) for asset, asset_infos in res.json().items()])
|
# return self.format_asset_info([asset['name'], self.get_asset_path(asset['name'], catalog) for asset, asset_infos in res.json().items()])
|
||||||
# pp(res.json())
|
# pp(res.json())
|
||||||
# pp(res2.json())
|
# pp(res2.json())
|
||||||
# print(res2)
|
# print(res2)
|
||||||
|
|
||||||
|
|
||||||
# return asset_infos
|
# return asset_infos
|
||||||
@@ -1,44 +1,41 @@
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
Plugin for making an asset library of all blender file found in a folder
|
Plugin for making an asset library of all blender file found in a folder
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from asset_library.library_types.library_type import LibraryType
|
||||||
|
from asset_library.common.bl_utils import load_datablocks
|
||||||
|
from asset_library.common.template import Template
|
||||||
|
|
||||||
|
import bpy
|
||||||
|
from bpy.props import StringProperty, IntProperty, BoolProperty
|
||||||
import re
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
from itertools import groupby
|
||||||
import uuid
|
import uuid
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import json
|
import json
|
||||||
import time
|
import time
|
||||||
from pathlib import Path
|
|
||||||
from itertools import groupby
|
|
||||||
|
|
||||||
import bpy
|
|
||||||
from bpy.props import (StringProperty, IntProperty, BoolProperty)
|
|
||||||
|
|
||||||
from asset_library.plugins.library_plugin import LibraryPlugin
|
|
||||||
from asset_library.core.bl_utils import load_datablocks
|
|
||||||
from asset_library.core.template import Template
|
|
||||||
|
|
||||||
|
|
||||||
|
class ScanFolder(LibraryType):
|
||||||
class ScanFolder(LibraryPlugin):
|
|
||||||
|
|
||||||
name = "Scan Folder"
|
name = "Scan Folder"
|
||||||
source_directory : StringProperty(subtype='DIR_PATH')
|
source_directory: StringProperty(subtype="DIR_PATH")
|
||||||
|
|
||||||
source_template_file : StringProperty()
|
source_template_file: StringProperty()
|
||||||
source_template_image : StringProperty()
|
source_template_image: StringProperty()
|
||||||
source_template_video : StringProperty()
|
source_template_video: StringProperty()
|
||||||
source_template_info : StringProperty()
|
source_template_info: StringProperty()
|
||||||
|
|
||||||
def draw_prefs(self, layout):
|
def draw_prefs(self, layout):
|
||||||
layout.prop(self, "source_directory", text="Source: Directory")
|
layout.prop(self, "source_directory", text="Source: Directory")
|
||||||
|
|
||||||
col = layout.column(align=True)
|
col = layout.column(align=True)
|
||||||
col.prop(self, "source_template_file", icon='COPY_ID', text='Template file')
|
col.prop(self, "source_template_file", icon="COPY_ID", text="Template file")
|
||||||
col.prop(self, "source_template_image", icon='COPY_ID', text='Template image')
|
col.prop(self, "source_template_image", icon="COPY_ID", text="Template image")
|
||||||
col.prop(self, "source_template_video", icon='COPY_ID', text='Template video')
|
col.prop(self, "source_template_video", icon="COPY_ID", text="Template video")
|
||||||
col.prop(self, "source_template_info", icon='COPY_ID', text='Template info')
|
col.prop(self, "source_template_info", icon="COPY_ID", text="Template info")
|
||||||
|
|
||||||
def get_asset_path(self, name, catalog, directory=None):
|
def get_asset_path(self, name, catalog, directory=None):
|
||||||
directory = directory or self.source_directory
|
directory = directory or self.source_directory
|
||||||
@@ -50,20 +47,26 @@ class ScanFolder(LibraryPlugin):
|
|||||||
def get_image_path(self, name, catalog, filepath):
|
def get_image_path(self, name, catalog, filepath):
|
||||||
catalog = self.norm_file_name(catalog)
|
catalog = self.norm_file_name(catalog)
|
||||||
name = self.norm_file_name(name)
|
name = self.norm_file_name(name)
|
||||||
return self.format_path(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):
|
def get_video_path(self, name, catalog, filepath):
|
||||||
catalog = self.norm_file_name(catalog)
|
catalog = self.norm_file_name(catalog)
|
||||||
name = self.norm_file_name(name)
|
name = self.norm_file_name(name)
|
||||||
return self.format_path(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):
|
def new_asset(self, asset, asset_data):
|
||||||
raise Exception('Need to be defined in the plugin')
|
raise Exception("Need to be defined in the library_type")
|
||||||
|
|
||||||
def remove_asset(self, asset, asset_data):
|
def remove_asset(self, asset, asset_data):
|
||||||
raise Exception('Need to be defined in the plugin')
|
raise Exception("Need to be defined in the library_type")
|
||||||
|
|
||||||
'''
|
"""
|
||||||
def format_asset_info(self, asset_datas, asset_path, modified=None):
|
def format_asset_info(self, asset_datas, asset_path, modified=None):
|
||||||
|
|
||||||
asset_path = self.prop_rel_path(asset_path, 'source_directory')
|
asset_path = self.prop_rel_path(asset_path, 'source_directory')
|
||||||
@@ -98,10 +101,10 @@ class ScanFolder(LibraryPlugin):
|
|||||||
name=asset_data['name']) for asset_data in asset_datas
|
name=asset_data['name']) for asset_data in asset_datas
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
'''
|
"""
|
||||||
|
|
||||||
def set_asset_preview(self, asset, asset_cache):
|
def set_asset_preview(self, asset, asset_cache):
|
||||||
'''Load an externalize image as preview for an asset using the source template'''
|
"""Load an externalize image as preview for an asset using the source template"""
|
||||||
|
|
||||||
asset_path = self.format_path(asset_cache.filepath)
|
asset_path = self.format_path(asset_cache.filepath)
|
||||||
|
|
||||||
@@ -109,15 +112,15 @@ class ScanFolder(LibraryPlugin):
|
|||||||
if not image_template:
|
if not image_template:
|
||||||
return
|
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:
|
if image_path:
|
||||||
with bpy.context.temp_override(id=asset):
|
with bpy.context.temp_override(id=asset):
|
||||||
bpy.ops.ed.lib_id_load_custom_preview(
|
bpy.ops.ed.lib_id_load_custom_preview(filepath=str(image_path))
|
||||||
filepath=str(image_path)
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
print(f'No image found for {image_template} on {asset.name}')
|
print(f"No image found for {image_template} on {asset.name}")
|
||||||
|
|
||||||
if asset.preview:
|
if asset.preview:
|
||||||
return asset.preview
|
return asset.preview
|
||||||
@@ -125,11 +128,11 @@ class ScanFolder(LibraryPlugin):
|
|||||||
def bundle(self, cache_diff=None):
|
def bundle(self, cache_diff=None):
|
||||||
"""Group all new assets in one or multiple blends for the asset browser"""
|
"""Group all new assets in one or multiple blends for the asset browser"""
|
||||||
|
|
||||||
if self.data_type not in ('FILE', 'ACTION', 'COLLECTION'):
|
if self.data_type not in ("FILE", "ACTION", "COLLECTION"):
|
||||||
print(f'{self.data_type} is not supported yet')
|
print(f"{self.data_type} is not supported yet")
|
||||||
return
|
return
|
||||||
|
|
||||||
#catalog_data = self.read_catalog()
|
# catalog_data = self.read_catalog()
|
||||||
|
|
||||||
catalog = self.read_catalog()
|
catalog = self.read_catalog()
|
||||||
cache = None
|
cache = None
|
||||||
@@ -141,59 +144,69 @@ class ScanFolder(LibraryPlugin):
|
|||||||
|
|
||||||
# Write the cache in a temporary file for the generate preview script
|
# Write the cache in a temporary file for the generate preview script
|
||||||
tmp_cache_file = cache.write(tmp=True)
|
tmp_cache_file = cache.write(tmp=True)
|
||||||
bpy.ops.assetlibrary.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)):
|
elif isinstance(cache_diff, (Path, str)):
|
||||||
cache_diff = json.loads(Path(cache_diff).read_text(encoding='utf-8'))
|
cache_diff = json.loads(Path(cache_diff).read_text(encoding="utf-8"))
|
||||||
|
|
||||||
if self.library.blend_depth == 0:
|
if self.library.blend_depth == 0:
|
||||||
raise Exception('Blender depth must be 1 at min')
|
raise Exception("Blender depth must be 1 at min")
|
||||||
|
|
||||||
total_assets = len(cache_diff)
|
total_assets = len(cache_diff)
|
||||||
print(f'total_assets={total_assets}')
|
print(f"total_assets={total_assets}")
|
||||||
|
|
||||||
if total_assets == 0:
|
if total_assets == 0:
|
||||||
print('No assets found')
|
print("No assets found")
|
||||||
return
|
return
|
||||||
|
|
||||||
i = 0
|
i = 0
|
||||||
for blend_path, asset_cache_diffs in cache_diff.group_by(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():
|
if blend_path.exists():
|
||||||
print(f'Opening existing bundle blend: {blend_path}')
|
print(f"Opening existing bundle blend: {blend_path}")
|
||||||
bpy.ops.wm.open_mainfile(filepath=str(blend_path))
|
bpy.ops.wm.open_mainfile(filepath=str(blend_path))
|
||||||
else:
|
else:
|
||||||
print(f'Create new bundle blend to: {blend_path}')
|
print(f"Create new bundle blend to: {blend_path}")
|
||||||
bpy.ops.wm.read_homefile(use_empty=True)
|
bpy.ops.wm.read_homefile(use_empty=True)
|
||||||
|
|
||||||
for asset_cache_diff in asset_cache_diffs:
|
for asset_cache_diff in asset_cache_diffs:
|
||||||
if total_assets <= 100 or i % int(total_assets / 10) == 0:
|
if total_assets <= 100 or i % int(total_assets / 10) == 0:
|
||||||
print(f'Progress: {int(i / total_assets * 100)+1}')
|
print(f"Progress: {int(i / total_assets * 100)+1}")
|
||||||
|
|
||||||
operation = asset_cache_diff.operation
|
operation = asset_cache_diff.operation
|
||||||
asset_cache = asset_cache_diff.asset_cache
|
asset_cache = asset_cache_diff.asset_cache
|
||||||
asset_name = asset_cache.name
|
asset_name = asset_cache.name
|
||||||
asset = getattr(bpy.data, self.data_types).get(asset_name)
|
asset = getattr(bpy.data, self.data_types).get(asset_name)
|
||||||
|
|
||||||
if operation == 'REMOVE':
|
if operation == "REMOVE":
|
||||||
if asset:
|
if asset:
|
||||||
getattr(bpy.data, self.data_types).remove(asset)
|
getattr(bpy.data, self.data_types).remove(asset)
|
||||||
else:
|
else:
|
||||||
print(f'ERROR : Remove Asset: {asset_name} not found in {blend_path}')
|
print(
|
||||||
|
f"ERROR : Remove Asset: {asset_name} not found in {blend_path}"
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if asset_cache_diff.operation == 'MODIFY' and not asset:
|
if asset_cache_diff.operation == "MODIFY" and not asset:
|
||||||
print(f'WARNING: Modifiy Asset: {asset_name} not found in {blend_path} it will be created')
|
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:
|
if asset:
|
||||||
#raise Exception(f"Asset {asset_name} Already in Blend")
|
# raise Exception(f"Asset {asset_name} Already in Blend")
|
||||||
print(f"Asset {asset_name} Already in Blend")
|
print(f"Asset {asset_name} Already in Blend")
|
||||||
getattr(bpy.data, self.data_types).remove(asset)
|
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)
|
asset = getattr(bpy.data, self.data_types).new(name=asset_name)
|
||||||
else:
|
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
|
continue
|
||||||
|
|
||||||
asset.asset_mark()
|
asset.asset_mark()
|
||||||
@@ -206,12 +219,11 @@ class ScanFolder(LibraryPlugin):
|
|||||||
|
|
||||||
i += 1
|
i += 1
|
||||||
|
|
||||||
print(f'Saving Blend to {blend_path}')
|
print(f"Saving Blend to {blend_path}")
|
||||||
|
|
||||||
blend_path.parent.mkdir(exist_ok=True, parents=True)
|
blend_path.parent.mkdir(exist_ok=True, parents=True)
|
||||||
bpy.ops.wm.save_as_mainfile(filepath=str(blend_path), compress=True)
|
bpy.ops.wm.save_as_mainfile(filepath=str(blend_path), compress=True)
|
||||||
|
|
||||||
|
|
||||||
# If the variable cache_diff was given we need to update the cache with the diff
|
# If the variable cache_diff was given we need to update the cache with the diff
|
||||||
if cache is None:
|
if cache is None:
|
||||||
cache = self.read_cache()
|
cache = self.read_cache()
|
||||||
@@ -227,32 +239,34 @@ class ScanFolder(LibraryPlugin):
|
|||||||
def fetch(self):
|
def fetch(self):
|
||||||
"""Gather in a list all assets found in the folder"""
|
"""Gather in a list all assets found in the folder"""
|
||||||
|
|
||||||
print(f'Fetch Assets for {self.library.name}')
|
print(f"Fetch Assets for {self.library.name}")
|
||||||
|
|
||||||
source_directory = Path(self.source_directory)
|
source_directory = Path(self.source_directory)
|
||||||
template_file = Template(self.source_template_file)
|
template_file = Template(self.source_template_file)
|
||||||
#catalog_data = self.read_catalog(directory=source_directory)
|
# catalog_data = self.read_catalog(directory=source_directory)
|
||||||
#catalog_ids = {v['id']: k for k, v in catalog_data.items()}
|
# catalog_ids = {v['id']: k for k, v in catalog_data.items()}
|
||||||
|
|
||||||
#self.catalog.read()
|
# self.catalog.read()
|
||||||
|
|
||||||
cache = self.read_cache()
|
cache = self.read_cache()
|
||||||
|
|
||||||
print(f'Search for blend using glob template: {template_file.glob_pattern}')
|
print(f"Search for blend using glob template: {template_file.glob_pattern}")
|
||||||
print(f'Scanning Folder {source_directory}...')
|
print(f"Scanning Folder {source_directory}...")
|
||||||
|
|
||||||
#new_cache = LibraryCache()
|
# new_cache = LibraryCache()
|
||||||
|
|
||||||
for asset_path in template_file.glob(source_directory):
|
for asset_path in template_file.glob(source_directory):
|
||||||
|
|
||||||
source_rel_path = self.prop_rel_path(asset_path, 'source_directory')
|
source_rel_path = self.prop_rel_path(asset_path, "source_directory")
|
||||||
modified = asset_path.stat().st_mtime_ns
|
modified = asset_path.stat().st_mtime_ns
|
||||||
|
|
||||||
# Check if the asset description as already been cached
|
# Check if the asset description as already been cached
|
||||||
file_cache = next((a for a in cache if a.filepath == source_rel_path), None)
|
file_cache = next((a for a in cache if a.filepath == source_rel_path), None)
|
||||||
|
|
||||||
if file_cache:
|
if file_cache:
|
||||||
if 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
|
continue
|
||||||
else:
|
else:
|
||||||
file_cache = cache.add(filepath=source_rel_path)
|
file_cache = cache.add(filepath=source_rel_path)
|
||||||
@@ -261,42 +275,42 @@ class ScanFolder(LibraryPlugin):
|
|||||||
field_data = template_file.parse(rel_path)
|
field_data = template_file.parse(rel_path)
|
||||||
|
|
||||||
# Create the catalog path from the actual path of the asset
|
# Create the catalog path from the actual path of the asset
|
||||||
catalog = [v for k,v in sorted(field_data.items()) if re.findall('cat[0-9]+', k)]
|
catalog = [
|
||||||
#catalogs = [c.replace('_', ' ').title() for c in catalogs]
|
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(
|
file_cache.set_data(
|
||||||
name=asset_name,
|
name=asset_name, type="FILE", catalog=catalog, modified=modified
|
||||||
type='FILE',
|
|
||||||
catalog=catalog,
|
|
||||||
modified=modified
|
|
||||||
)
|
)
|
||||||
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Now check if there is a asset description file (Commented for now propably not usefull)
|
# 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)
|
# asset_info_path = self.find_path(self.source_template_info, asset_info, filepath=asset_path)
|
||||||
#if asset_info_path:
|
# if asset_info_path:
|
||||||
# new_cache.append(self.read_file(asset_info_path))
|
# new_cache.append(self.read_file(asset_info_path))
|
||||||
# continue
|
# continue
|
||||||
|
|
||||||
# Scan the blend file for assets inside
|
# Scan the blend file for assets inside
|
||||||
print(f'Scanning blendfile {asset_path}...')
|
print(f"Scanning blendfile {asset_path}...")
|
||||||
assets = self.load_datablocks(asset_path, type=self.data_types, link=True, assets_only=True)
|
assets = self.load_datablocks(
|
||||||
print(f'Found {len(assets)} {self.data_types} inside')
|
asset_path, type=self.data_types, link=True, assets_only=True
|
||||||
|
)
|
||||||
|
print(f"Found {len(assets)} {self.data_types} inside")
|
||||||
|
|
||||||
for asset in assets:
|
for asset in assets:
|
||||||
#catalog_path = catalog_ids.get(asset.asset_data.catalog_id)
|
# catalog_path = catalog_ids.get(asset.asset_data.catalog_id)
|
||||||
|
|
||||||
#if not catalog_path:
|
# if not catalog_path:
|
||||||
# print(f'No catalog found for asset {asset.name}')
|
# 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
|
# For now the catalog used is the one extract from the template file
|
||||||
file_cache.assets.add(self.get_asset_data(asset), catalog=catalog)
|
file_cache.assets.add(self.get_asset_data(asset), catalog=catalog)
|
||||||
getattr(bpy.data, self.data_types).remove(asset)
|
getattr(bpy.data, self.data_types).remove(asset)
|
||||||
|
|
||||||
return cache
|
return cache
|
||||||
|
|
||||||
+710
-540
File diff suppressed because it is too large
Load Diff
@@ -1,49 +0,0 @@
|
|||||||
|
|
||||||
"""
|
|
||||||
Adapter for making an asset library of all blender file found in a folder
|
|
||||||
"""
|
|
||||||
|
|
||||||
from os.path import expandvars
|
|
||||||
|
|
||||||
import bpy
|
|
||||||
from bpy.props import StringProperty
|
|
||||||
|
|
||||||
from asset_library.plugins.library_plugin import LibraryPlugin
|
|
||||||
from asset_library.core.file_utils import copy_dir
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class CopyFolder(LibraryPlugin):
|
|
||||||
"""Copy library folder from a server to a local disk for better performance"""
|
|
||||||
|
|
||||||
name = "Copy Folder"
|
|
||||||
source_directory : StringProperty()
|
|
||||||
|
|
||||||
includes : StringProperty()
|
|
||||||
excludes : StringProperty()
|
|
||||||
|
|
||||||
def bundle(self, cache_diff=None):
|
|
||||||
src = expandvars(self.source_directory)
|
|
||||||
dst = expandvars(self.bundle_directory)
|
|
||||||
|
|
||||||
includes = [inc.strip() for inc in self.includes.split(',')]
|
|
||||||
excludes = [ex.strip() for ex in self.excludes.split(',')]
|
|
||||||
|
|
||||||
print(f'Copy Folder from {src} to {dst}...')
|
|
||||||
copy_dir(
|
|
||||||
src, dst, only_recent=True,
|
|
||||||
excludes=excludes, includes=includes
|
|
||||||
)
|
|
||||||
|
|
||||||
def filter_prop(self, prop):
|
|
||||||
if prop in ('template_info', 'template_video', 'template_image', 'blend_depth'):
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
# def draw_prop(self, layout, prop):
|
|
||||||
# if prop in ('template_info', 'template_video', 'template_image', 'blend_depth'):
|
|
||||||
# return
|
|
||||||
|
|
||||||
# super().draw_prop(layout)
|
|
||||||
@@ -1,269 +0,0 @@
|
|||||||
|
|
||||||
"""
|
|
||||||
Plugin for making an asset library of all blender file found in a folder
|
|
||||||
"""
|
|
||||||
|
|
||||||
import re
|
|
||||||
from pathlib import Path
|
|
||||||
from itertools import groupby
|
|
||||||
import uuid
|
|
||||||
import os
|
|
||||||
import shutil
|
|
||||||
import json
|
|
||||||
import urllib3
|
|
||||||
import traceback
|
|
||||||
import time
|
|
||||||
|
|
||||||
import bpy
|
|
||||||
from bpy.props import (StringProperty, IntProperty, BoolProperty)
|
|
||||||
|
|
||||||
from asset_library.plugins.library_plugin import LibraryPlugin
|
|
||||||
from asset_library.core.template import Template
|
|
||||||
from asset_library.core.file_utils import install_module
|
|
||||||
|
|
||||||
|
|
||||||
class Kitsu(LibraryPlugin):
|
|
||||||
|
|
||||||
name = "Kitsu"
|
|
||||||
template_name : StringProperty()
|
|
||||||
template_file : StringProperty()
|
|
||||||
source_directory : StringProperty(subtype='DIR_PATH')
|
|
||||||
#blend_depth: IntProperty(default=1)
|
|
||||||
source_template_image : StringProperty()
|
|
||||||
target_template_image : StringProperty()
|
|
||||||
|
|
||||||
url: StringProperty()
|
|
||||||
login: StringProperty()
|
|
||||||
password: StringProperty(subtype='PASSWORD')
|
|
||||||
project_name: StringProperty()
|
|
||||||
|
|
||||||
def connect(self, url=None, login=None, password=None):
|
|
||||||
'''Connect to kitsu api using provided url, login and password'''
|
|
||||||
|
|
||||||
gazu = install_module('gazu')
|
|
||||||
urllib3.disable_warnings()
|
|
||||||
|
|
||||||
if not self.url:
|
|
||||||
print(f'Kitsu Url: {self.url} is empty')
|
|
||||||
return
|
|
||||||
|
|
||||||
url = self.url
|
|
||||||
if not url.endswith('/api'):
|
|
||||||
url += '/api'
|
|
||||||
|
|
||||||
print(f'Info: Setting Host for kitsu {url}')
|
|
||||||
gazu.client.set_host(url)
|
|
||||||
|
|
||||||
if not gazu.client.host_is_up():
|
|
||||||
print('Error: Kitsu Host is down')
|
|
||||||
|
|
||||||
try:
|
|
||||||
print(f'Info: Log in to kitsu as {self.login}')
|
|
||||||
res = gazu.log_in(self.login, self.password)
|
|
||||||
print(f'Info: Sucessfully login to Kitsu as {res["user"]["full_name"]}')
|
|
||||||
return res['user']
|
|
||||||
except Exception as e:
|
|
||||||
print(f'Error: {traceback.format_exc()}')
|
|
||||||
|
|
||||||
def get_asset_path(self, name, catalog, directory=None):
|
|
||||||
directory = directory or self.source_directory
|
|
||||||
return Path(directory, self.get_asset_relative_path(name, catalog))
|
|
||||||
|
|
||||||
def get_asset_info(self, data, asset_path):
|
|
||||||
|
|
||||||
modified = time.time_ns()
|
|
||||||
catalog = data['entity_type_name'].title()
|
|
||||||
asset_path = self.prop_rel_path(asset_path, 'source_directory')
|
|
||||||
#asset_name = self.norm_file_name(data['name'])
|
|
||||||
|
|
||||||
asset_info = dict(
|
|
||||||
filepath=asset_path,
|
|
||||||
modified=modified,
|
|
||||||
library_id=self.library.id,
|
|
||||||
assets=[dict(
|
|
||||||
catalog=catalog,
|
|
||||||
metadata=data.get('data', {}),
|
|
||||||
description=data['description'],
|
|
||||||
tags=[],
|
|
||||||
type=self.data_type,
|
|
||||||
#image=self.library.template_image,
|
|
||||||
#video=self.library.template_video,
|
|
||||||
name=data['name'])
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
return asset_info
|
|
||||||
|
|
||||||
# def bundle(self, cache_diff=None):
|
|
||||||
# """Group all asset in one or multiple blends for the asset browser"""
|
|
||||||
|
|
||||||
# return super().bundle(cache_diff=cache_diff)
|
|
||||||
|
|
||||||
def set_asset_preview(self, asset, asset_data):
|
|
||||||
'''Load an externalize image as preview for an asset using the source template'''
|
|
||||||
|
|
||||||
asset_path = self.format_path(Path(asset_data['filepath']).as_posix())
|
|
||||||
|
|
||||||
image_path = self.find_path(self.target_template_image, asset_data, filepath=asset_path)
|
|
||||||
|
|
||||||
if image_path:
|
|
||||||
with bpy.context.temp_override(id=asset):
|
|
||||||
bpy.ops.ed.lib_id_load_custom_preview(
|
|
||||||
filepath=str(image_path)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
print(f'No image found for {self.target_template_image} on {asset.name}')
|
|
||||||
|
|
||||||
if asset.preview:
|
|
||||||
return asset.preview
|
|
||||||
|
|
||||||
|
|
||||||
def generate_previews(self, cache=None):
|
|
||||||
|
|
||||||
print('Generate previews...')
|
|
||||||
|
|
||||||
if cache in (None, ''):
|
|
||||||
cache = self.fetch()
|
|
||||||
elif isinstance(cache, (Path, str)):
|
|
||||||
cache = self.read_cache(cache)
|
|
||||||
|
|
||||||
#TODO Support all multiple data_type
|
|
||||||
for asset_info in cache:
|
|
||||||
|
|
||||||
if asset_info.get('type', self.data_type) == 'FILE':
|
|
||||||
self.generate_blend_preview(asset_info)
|
|
||||||
else:
|
|
||||||
self.generate_asset_preview(asset_info)
|
|
||||||
|
|
||||||
def generate_asset_preview(self, asset_info):
|
|
||||||
|
|
||||||
data_type = self.data_type
|
|
||||||
scn = bpy.context.scene
|
|
||||||
vl = bpy.context.view_layer
|
|
||||||
|
|
||||||
asset_path = self.format_path(asset_info['filepath'])
|
|
||||||
|
|
||||||
lens = 85
|
|
||||||
|
|
||||||
if not asset_path.exists():
|
|
||||||
print(f'Blend file {asset_path} not exit')
|
|
||||||
return
|
|
||||||
|
|
||||||
|
|
||||||
asset_data_names = {}
|
|
||||||
|
|
||||||
# First check wich assets need a preview
|
|
||||||
for asset_data in asset_info['assets']:
|
|
||||||
name = asset_data['name']
|
|
||||||
image_path = self.format_path(self.target_template_image, asset_data, filepath=asset_path)
|
|
||||||
|
|
||||||
if image_path.exists():
|
|
||||||
continue
|
|
||||||
|
|
||||||
#Store in a dict all asset_data that does not have preview
|
|
||||||
asset_data_names[name] = dict(asset_data, image_path=image_path)
|
|
||||||
|
|
||||||
if not asset_data_names:
|
|
||||||
print(f'All previews already existing for {asset_path}')
|
|
||||||
return
|
|
||||||
|
|
||||||
#asset_names = [a['name'] for a in asset_info['assets']]
|
|
||||||
asset_names = list(asset_data_names.keys())
|
|
||||||
assets = self.load_datablocks(asset_path, names=asset_names, link=True, type=data_type)
|
|
||||||
|
|
||||||
print(asset_names)
|
|
||||||
print(assets)
|
|
||||||
|
|
||||||
for asset in assets:
|
|
||||||
if not asset:
|
|
||||||
continue
|
|
||||||
|
|
||||||
print(f'Generate Preview for asset {asset.name}')
|
|
||||||
|
|
||||||
asset_data = asset_data_names[asset.name]
|
|
||||||
|
|
||||||
#print(self.target_template_image, asset_path)
|
|
||||||
image_path = self.format_path(self.target_template_image, asset_data, filepath=asset_path)
|
|
||||||
|
|
||||||
# Force redo preview
|
|
||||||
# if asset.preview:
|
|
||||||
# print(f'Writing asset preview to {image_path}')
|
|
||||||
# self.write_preview(asset.preview, image_path)
|
|
||||||
# continue
|
|
||||||
|
|
||||||
if data_type == 'COLLECTION':
|
|
||||||
|
|
||||||
bpy.ops.object.collection_instance_add(name=asset.name)
|
|
||||||
|
|
||||||
scn.camera.data.lens = lens
|
|
||||||
bpy.ops.view3d.camera_to_view_selected()
|
|
||||||
scn.camera.data.lens -= 5
|
|
||||||
|
|
||||||
instance = vl.objects.active
|
|
||||||
|
|
||||||
#scn.collection.children.link(asset)
|
|
||||||
|
|
||||||
scn.render.filepath = str(image_path)
|
|
||||||
scn.render.image_settings.file_format = self.format_from_ext(image_path.suffix)
|
|
||||||
scn.render.image_settings.color_mode = 'RGBA'
|
|
||||||
scn.render.image_settings.quality = 90
|
|
||||||
|
|
||||||
|
|
||||||
print(f'Render asset {asset.name} to {image_path}')
|
|
||||||
bpy.ops.render.render(write_still=True)
|
|
||||||
|
|
||||||
#instance.user_clear()
|
|
||||||
asset.user_clear()
|
|
||||||
|
|
||||||
bpy.data.objects.remove(instance)
|
|
||||||
|
|
||||||
bpy.ops.outliner.orphans_purge(do_local_ids=True, do_linked_ids=True, do_recursive=True)
|
|
||||||
|
|
||||||
def fetch(self):
|
|
||||||
"""Gather in a list all assets found in the folder"""
|
|
||||||
|
|
||||||
print(f'Fetch Assets for {self.library.name}')
|
|
||||||
|
|
||||||
gazu = install_module('gazu')
|
|
||||||
self.connect()
|
|
||||||
|
|
||||||
template_file = Template(self.template_file)
|
|
||||||
template_name = Template(self.template_name)
|
|
||||||
|
|
||||||
project = gazu.client.fetch_first('projects', {'name': self.project_name})
|
|
||||||
entity_types = gazu.client.fetch_all('entity-types')
|
|
||||||
entity_types_ids = {e['id']: e['name'] for e in entity_types}
|
|
||||||
|
|
||||||
cache = self.read_cache()
|
|
||||||
|
|
||||||
for asset_data in gazu.asset.all_assets_for_project(project):
|
|
||||||
asset_data['entity_type_name'] = entity_types_ids[asset_data.pop('entity_type_id')]
|
|
||||||
asset_name = asset_data['name']
|
|
||||||
|
|
||||||
asset_field_data = dict(asset_name=asset_name, type=asset_data['entity_type_name'], source_directory=self.source_directory)
|
|
||||||
|
|
||||||
try:
|
|
||||||
asset_field_data.update(template_name.parse(asset_name))
|
|
||||||
except Exception:
|
|
||||||
print(f'Warning: Could not parse {asset_name} with template {template_name}')
|
|
||||||
|
|
||||||
asset_path = template_file.find(asset_field_data)
|
|
||||||
if not asset_path:
|
|
||||||
print(f'Warning: Could not find file for {template_file.format(asset_field_data)}')
|
|
||||||
continue
|
|
||||||
|
|
||||||
|
|
||||||
asset_path = self.prop_rel_path(asset_path, 'source_directory')
|
|
||||||
asset_cache_data = dict(
|
|
||||||
catalog=asset_data['entity_type_name'].title(),
|
|
||||||
metadata=asset_data.get('data', {}),
|
|
||||||
description=asset_data['description'],
|
|
||||||
tags=[],
|
|
||||||
type=self.data_type,
|
|
||||||
name=asset_data['name']
|
|
||||||
)
|
|
||||||
|
|
||||||
cache.add_asset_cache(asset_cache_data, filepath=asset_path)
|
|
||||||
|
|
||||||
|
|
||||||
return cache
|
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
|
from asset_library.pose import operators
|
||||||
|
|
||||||
from asset_library.pose import (
|
if "bpy" in locals():
|
||||||
operators)
|
|
||||||
|
|
||||||
if 'bpy' in locals():
|
|
||||||
import importlib
|
import importlib
|
||||||
|
|
||||||
importlib.reload(operators)
|
importlib.reload(operators)
|
||||||
|
|
||||||
|
|
||||||
def register():
|
def register():
|
||||||
operators.register()
|
operators.register()
|
||||||
|
|
||||||
|
|
||||||
def unregister():
|
def unregister():
|
||||||
operators.unregister()
|
operators.unregister()
|
||||||
@@ -39,7 +39,7 @@ def convert_old_poselib(old_poselib: Action) -> Collection[Action]:
|
|||||||
# appropriate frame in the scene (to set up things like the background
|
# appropriate frame in the scene (to set up things like the background
|
||||||
# colour), but the old-style poselib doesn't contain such information. All
|
# colour), but the old-style poselib doesn't contain such information. All
|
||||||
# we can do is just render on the current frame.
|
# we can do is just render on the current frame.
|
||||||
bpy.ops.asset.mark({'selected_ids': pose_assets})
|
bpy.ops.asset.mark({"selected_ids": pose_assets})
|
||||||
|
|
||||||
return pose_assets
|
return pose_assets
|
||||||
|
|
||||||
@@ -22,7 +22,13 @@ import subprocess
|
|||||||
import uuid
|
import uuid
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from bpy.props import BoolProperty, CollectionProperty, EnumProperty, PointerProperty, StringProperty
|
from bpy.props import (
|
||||||
|
BoolProperty,
|
||||||
|
CollectionProperty,
|
||||||
|
EnumProperty,
|
||||||
|
PointerProperty,
|
||||||
|
StringProperty,
|
||||||
|
)
|
||||||
from bpy.types import (
|
from bpy.types import (
|
||||||
Action,
|
Action,
|
||||||
Context,
|
Context,
|
||||||
@@ -35,16 +41,12 @@ from bpy.types import (
|
|||||||
from bpy_extras import asset_utils
|
from bpy_extras import asset_utils
|
||||||
from bpy_extras.io_utils import ExportHelper, ImportHelper
|
from bpy_extras.io_utils import ExportHelper, ImportHelper
|
||||||
|
|
||||||
from asset_library.data_type.action.functions import (
|
from asset_library.action.functions import (
|
||||||
get_marker,
|
get_marker,
|
||||||
get_keyframes,
|
get_keyframes,
|
||||||
)
|
)
|
||||||
|
|
||||||
from asset_library.common.bl_utils import (
|
from asset_library.common.bl_utils import get_view3d_persp, load_assets_from, split_path
|
||||||
get_view3d_persp,
|
|
||||||
load_assets_from,
|
|
||||||
split_path
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class POSELIB_OT_create_pose_asset(Operator):
|
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
|
pose_name: StringProperty(name="Pose Name") # type: ignore
|
||||||
activate_new_action: BoolProperty(name="Activate New Action", default=True) # type: ignore
|
activate_new_action: BoolProperty(name="Activate New Action", default=True) # type: ignore
|
||||||
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context: Context) -> bool:
|
def poll(cls, context: Context) -> bool:
|
||||||
# Make sure that if there is an asset browser open, the artist can see the newly created pose asset.
|
# Make sure that if there is an asset browser open, the artist can see the newly created pose asset.
|
||||||
asset_browse_area: Optional[bpy.types.Area] = asset_browser.area_from_context(context)
|
asset_browse_area: Optional[bpy.types.Area] = asset_browser.area_from_context(
|
||||||
|
context
|
||||||
|
)
|
||||||
if not asset_browse_area:
|
if not asset_browse_area:
|
||||||
# No asset browser is visible, so there also aren't any expectations
|
# No asset browser is visible, so there also aren't any expectations
|
||||||
# that this asset will be visible.
|
# that this asset will be visible.
|
||||||
return True
|
return True
|
||||||
|
|
||||||
asset_space_params = asset_browser.params(asset_browse_area)
|
asset_space_params = asset_browser.params(asset_browse_area)
|
||||||
if asset_space_params.asset_library_ref != 'LOCAL':
|
if asset_space_params.asset_library_ref != "LOCAL":
|
||||||
cls.poll_message_set("Asset Browser must be set to the Current File library")
|
cls.poll_message_set(
|
||||||
|
"Asset Browser must be set to the Current File library"
|
||||||
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def execute(self, context: Context) -> Set[str]:
|
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
|
pose_name = False
|
||||||
if context.object.animation_data:
|
if context.object.animation_data:
|
||||||
if context.object.animation_data.action:
|
if context.object.animation_data.action:
|
||||||
@@ -85,28 +90,28 @@ class POSELIB_OT_create_pose_asset(Operator):
|
|||||||
|
|
||||||
if pose_name:
|
if pose_name:
|
||||||
prefix = True
|
prefix = True
|
||||||
asset_name = Path(bpy.data.filepath).stem.split('_')[0]
|
asset_name = Path(bpy.data.filepath).stem.split("_")[0]
|
||||||
|
|
||||||
action_asset_name = re.search(f'^{asset_name}.', pose_name)
|
action_asset_name = re.search(f"^{asset_name}.", pose_name)
|
||||||
if action_asset_name:
|
if action_asset_name:
|
||||||
pose_name = pose_name.replace(action_asset_name.group(0), '')
|
pose_name = pose_name.replace(action_asset_name.group(0), "")
|
||||||
|
|
||||||
side = re.search('_\w$', pose_name)
|
side = re.search("_\w$", pose_name)
|
||||||
if side:
|
if side:
|
||||||
pose_name = pose_name.replace(side.group(0), '')
|
pose_name = pose_name.replace(side.group(0), "")
|
||||||
|
|
||||||
if 'hands' in context.object.animation_data.action.name.lower():
|
if "hands" in context.object.animation_data.action.name.lower():
|
||||||
pose_name = f'hand_{pose_name}'
|
pose_name = f"hand_{pose_name}"
|
||||||
|
|
||||||
if pose_name.startswith('lips_'):
|
if pose_name.startswith("lips_"):
|
||||||
pose_name.replace('lips_', '')
|
pose_name.replace("lips_", "")
|
||||||
split = pose_name.split('_')
|
split = pose_name.split("_")
|
||||||
pose_name = '-'.join([s for s in split if s.isupper()])
|
pose_name = "-".join([s for s in split if s.isupper()])
|
||||||
pose_name = f'{pose_name}_{split[-1]}'
|
pose_name = f"{pose_name}_{split[-1]}"
|
||||||
prefix = False
|
prefix = False
|
||||||
|
|
||||||
if prefix and not pose_name.startswith(asset_name):
|
if prefix and not pose_name.startswith(asset_name):
|
||||||
pose_name = f'{asset_name}_{pose_name}'
|
pose_name = f"{asset_name}_{pose_name}"
|
||||||
|
|
||||||
else:
|
else:
|
||||||
pose_name = self.pose_name or context.object.name
|
pose_name = self.pose_name or context.object.name
|
||||||
@@ -126,7 +131,6 @@ class POSELIB_OT_create_pose_asset(Operator):
|
|||||||
if context.scene.camera:
|
if context.scene.camera:
|
||||||
data_dict.update(dict(camera=context.scene.camera.name))
|
data_dict.update(dict(camera=context.scene.camera.name))
|
||||||
|
|
||||||
|
|
||||||
for k, v in data_dict.items():
|
for k, v in data_dict.items():
|
||||||
data[k] = v
|
data[k] = v
|
||||||
###
|
###
|
||||||
@@ -134,7 +138,7 @@ class POSELIB_OT_create_pose_asset(Operator):
|
|||||||
if self.activate_new_action:
|
if self.activate_new_action:
|
||||||
self._set_active_action(context, asset)
|
self._set_active_action(context, asset)
|
||||||
self._activate_asset_in_browser(context, asset)
|
self._activate_asset_in_browser(context, asset)
|
||||||
return {'FINISHED'}
|
return {"FINISHED"}
|
||||||
|
|
||||||
def _set_active_action(self, context: Context, asset: Action) -> None:
|
def _set_active_action(self, context: Context, asset: Action) -> None:
|
||||||
self._prevent_action_loss(context.object)
|
self._prevent_action_loss(context.object)
|
||||||
@@ -149,7 +153,9 @@ class POSELIB_OT_create_pose_asset(Operator):
|
|||||||
This makes it possible to immediately check & edit the created pose asset.
|
This makes it possible to immediately check & edit the created pose asset.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
asset_browse_area: Optional[bpy.types.Area] = asset_browser.area_from_context(context)
|
asset_browse_area: Optional[bpy.types.Area] = asset_browser.area_from_context(
|
||||||
|
context
|
||||||
|
)
|
||||||
if not asset_browse_area:
|
if not asset_browse_area:
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -181,7 +187,9 @@ class POSELIB_OT_create_pose_asset(Operator):
|
|||||||
return
|
return
|
||||||
|
|
||||||
action.use_fake_user = True
|
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):
|
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)
|
self._timer = wm.event_timer_add(0.001, window=context.window)
|
||||||
wm.modal_handler_add(self)
|
wm.modal_handler_add(self)
|
||||||
|
|
||||||
return {'RUNNING_MODAL'}
|
return {"RUNNING_MODAL"}
|
||||||
|
|
||||||
def modal(self, context, event):
|
def modal(self, context, event):
|
||||||
if event.type != 'TIMER':
|
if event.type != "TIMER":
|
||||||
return {'RUNNING_MODAL'}
|
return {"RUNNING_MODAL"}
|
||||||
|
|
||||||
wm = context.window_manager
|
wm = context.window_manager
|
||||||
wm.event_timer_remove(self._timer)
|
wm.event_timer_remove(self._timer)
|
||||||
|
|
||||||
context.object.pose.apply_pose_from_action(self.pose_action)
|
context.object.pose.apply_pose_from_action(self.pose_action)
|
||||||
return {'FINISHED'}
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
class ASSET_OT_assign_action(Operator):
|
class ASSET_OT_assign_action(Operator):
|
||||||
@@ -257,7 +265,9 @@ class ASSET_OT_assign_action(Operator):
|
|||||||
class POSELIB_OT_copy_as_asset(Operator):
|
class POSELIB_OT_copy_as_asset(Operator):
|
||||||
bl_idname = "poselib.copy_as_asset"
|
bl_idname = "poselib.copy_as_asset"
|
||||||
bl_label = "Copy Pose As Asset"
|
bl_label = "Copy Pose As Asset"
|
||||||
bl_description = "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"}
|
bl_options = {"REGISTER"}
|
||||||
|
|
||||||
CLIPBOARD_ASSET_MARKER = "ASSET-BLEND="
|
CLIPBOARD_ASSET_MARKER = "ASSET-BLEND="
|
||||||
@@ -289,7 +299,10 @@ class POSELIB_OT_copy_as_asset(Operator):
|
|||||||
filepath,
|
filepath,
|
||||||
)
|
)
|
||||||
asset_browser.tag_redraw(context.screen)
|
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.
|
# The asset has been saved to disk, so to clean up it has to loose its asset & fake user status.
|
||||||
asset.asset_clear()
|
asset.asset_clear()
|
||||||
@@ -300,7 +313,10 @@ class POSELIB_OT_copy_as_asset(Operator):
|
|||||||
if asset.users > 0:
|
if asset.users > 0:
|
||||||
# This should never happen, and indicates a bug in the code. Having a warning about it is nice,
|
# This should never happen, and indicates a bug in the code. Having a warning about it is nice,
|
||||||
# but it shouldn't stand in the way of actually cleaning up the meant-to-be-temporary datablock.
|
# but it shouldn't stand in the way of actually cleaning up the meant-to-be-temporary datablock.
|
||||||
self.report({"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)
|
bpy.data.actions.remove(asset)
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
@@ -331,8 +347,10 @@ class POSELIB_OT_paste_asset(Operator):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
asset_lib_ref = context.space_data.params.asset_library_ref
|
asset_lib_ref = context.space_data.params.asset_library_ref
|
||||||
if asset_lib_ref != 'LOCAL':
|
if asset_lib_ref != "LOCAL":
|
||||||
cls.poll_message_set("Asset Browser must be set to the Current File library")
|
cls.poll_message_set(
|
||||||
|
"Asset Browser must be set to the Current File library"
|
||||||
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Delay checking the clipboard as much as possible, as it's CPU-heavier than the other checks.
|
# Delay checking the clipboard as much as possible, as it's CPU-heavier than the other checks.
|
||||||
@@ -348,7 +366,6 @@ class POSELIB_OT_paste_asset(Operator):
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def execute(self, context: Context) -> Set[str]:
|
def execute(self, context: Context) -> Set[str]:
|
||||||
clipboard = context.window_manager.clipboard
|
clipboard = context.window_manager.clipboard
|
||||||
marker_len = len(POSELIB_OT_copy_as_asset.CLIPBOARD_ASSET_MARKER)
|
marker_len = len(POSELIB_OT_copy_as_asset.CLIPBOARD_ASSET_MARKER)
|
||||||
@@ -379,18 +396,18 @@ class POSELIB_OT_paste_asset(Operator):
|
|||||||
class POSELIB_OT_pose_asset_select_bones(Operator):
|
class POSELIB_OT_pose_asset_select_bones(Operator):
|
||||||
bl_idname = "poselib.pose_asset_select_bones"
|
bl_idname = "poselib.pose_asset_select_bones"
|
||||||
bl_label = "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_description = "Click: Select used Bones\nAlt+Click: Select Flipped Bones\nCtrl+Click: Select Both sides."
|
||||||
bl_options = {"REGISTER", "UNDO"}
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
#bl_property = "selected_side"
|
# bl_property = "selected_side"
|
||||||
|
|
||||||
selected_side: EnumProperty(
|
selected_side: EnumProperty(
|
||||||
name='Selected Side',
|
name="Selected Side",
|
||||||
items=(
|
items=(
|
||||||
('CURRENT', "Current", ""),
|
("CURRENT", "Current", ""),
|
||||||
('FLIPPED', "Flipped", ""),
|
("FLIPPED", "Flipped", ""),
|
||||||
('BOTH', "Both", ""),
|
("BOTH", "Both", ""),
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -402,7 +419,7 @@ class POSELIB_OT_pose_asset_select_bones(Operator):
|
|||||||
and context.asset_file_handle
|
and context.asset_file_handle
|
||||||
):
|
):
|
||||||
return False
|
return False
|
||||||
return context.asset_file_handle.id_type == 'ACTION'
|
return context.asset_file_handle.id_type == "ACTION"
|
||||||
|
|
||||||
def execute(self, context: Context) -> Set[str]:
|
def execute(self, context: Context) -> Set[str]:
|
||||||
asset: FileSelectEntry = context.asset_file_handle
|
asset: FileSelectEntry = context.asset_file_handle
|
||||||
@@ -417,7 +434,9 @@ class POSELIB_OT_pose_asset_select_bones(Operator):
|
|||||||
def _load_and_use_pose(self, context: Context) -> Set[str]:
|
def _load_and_use_pose(self, context: Context) -> Set[str]:
|
||||||
asset_library_ref = context.asset_library_ref
|
asset_library_ref = context.asset_library_ref
|
||||||
asset = context.asset_file_handle
|
asset = context.asset_file_handle
|
||||||
asset_lib_path = bpy.types.AssetHandle.get_full_library_path(asset, asset_library_ref)
|
asset_lib_path = bpy.types.AssetHandle.get_full_library_path(
|
||||||
|
asset, asset_library_ref
|
||||||
|
)
|
||||||
|
|
||||||
if not asset_lib_path:
|
if not asset_lib_path:
|
||||||
self.report( # type: ignore
|
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",
|
f"Selected asset {asset.name} could not be located inside the asset library",
|
||||||
)
|
)
|
||||||
return {"CANCELLED"}
|
return {"CANCELLED"}
|
||||||
if asset.id_type != 'ACTION':
|
if asset.id_type != "ACTION":
|
||||||
self.report( # type: ignore
|
self.report( # type: ignore
|
||||||
{"ERROR"},
|
{"ERROR"},
|
||||||
f"Selected asset {asset.name} is not an Action",
|
f"Selected asset {asset.name} is not an Action",
|
||||||
@@ -442,10 +461,13 @@ class POSELIB_OT_pose_asset_select_bones(Operator):
|
|||||||
|
|
||||||
def use_pose(self, context: Context, pose_asset: Action) -> Set[str]:
|
def use_pose(self, context: Context, pose_asset: Action) -> Set[str]:
|
||||||
arm_object: Object = context.object
|
arm_object: Object = context.object
|
||||||
#pose_usage.select_bones(arm_object, pose_asset, select=self.select, flipped=self.flipped)
|
# pose_usage.select_bones(arm_object, pose_asset, select=self.select, flipped=self.flipped)
|
||||||
pose_usage.select_bones(arm_object, pose_asset, selected_side=self.selected_side)
|
pose_usage.select_bones(
|
||||||
|
arm_object, pose_asset, selected_side=self.selected_side
|
||||||
|
)
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
# This operator takes the Window Manager's `actionlib_flipped` property, and
|
# This operator takes the Window Manager's `actionlib_flipped` property, and
|
||||||
# passes it to the `POSELIB_OT_blend_pose_asset` operator. This makes it
|
# passes it to the `POSELIB_OT_blend_pose_asset` operator. This makes it
|
||||||
# possible to bind a key to the operator and still have it respect the global
|
# possible to bind a key to the operator and still have it respect the global
|
||||||
@@ -478,10 +500,14 @@ class POSELIB_OT_blend_pose_asset_for_keymap(Operator):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def invoke(self, context: Context, event: Event) -> Set[str]:
|
def invoke(self, context: Context, event: Event) -> Set[str]:
|
||||||
return bpy.ops.poselib.blend_pose_asset(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]:
|
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
|
# 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
|
# possible to bind a key to the operator and still have it respect the global
|
||||||
# "Flip Pose" checkbox.
|
# "Flip Pose" checkbox.
|
||||||
|
|
||||||
|
|
||||||
class POSELIB_OT_apply_pose_asset_for_keymap(Operator):
|
class POSELIB_OT_apply_pose_asset_for_keymap(Operator):
|
||||||
bl_idname = "poselib.apply_pose_asset_for_keymap"
|
bl_idname = "poselib.apply_pose_asset_for_keymap"
|
||||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||||
|
|
||||||
_rna = bpy.ops.poselib.apply_pose_asset.get_rna_type()
|
_rna = bpy.ops.poselib.apply_pose_asset.get_rna_type()
|
||||||
bl_label = _rna.name
|
bl_label = _rna.name
|
||||||
#bl_description = _rna.description
|
# bl_description = _rna.description
|
||||||
bl_description = 'Apply Pose to Bones'
|
bl_description = "Apply Pose to Bones"
|
||||||
del _rna
|
del _rna
|
||||||
|
|
||||||
flipped: BoolProperty(name="Flipped", default=False) # type: ignore
|
flipped: BoolProperty(name="Flipped", default=False) # type: ignore
|
||||||
@@ -514,19 +541,34 @@ class POSELIB_OT_apply_pose_asset_for_keymap(Operator):
|
|||||||
store_bones = {}
|
store_bones = {}
|
||||||
|
|
||||||
bones = [
|
bones = [
|
||||||
'blendshape-eyes', 'blendshape-eye.L', 'blendshape-eye.R',
|
"blendshape-eyes",
|
||||||
'blendshape-corner-mouth', 'blendshape-corner-mouth.L',
|
"blendshape-eye.L",
|
||||||
'blendshape-corner-down-mouth.L', 'blendshape-corner-up-mouth.L',
|
"blendshape-eye.R",
|
||||||
'blendshape-corner-mouth-add.L','blendshape-corner-mouth.R',
|
"blendshape-corner-mouth",
|
||||||
'blendshape-corner-down-mouth.R', 'blendshape-corner-up-mouth.R',
|
"blendshape-corner-mouth.L",
|
||||||
'blendshape-corner-mouth-add.R', 'blendshape-center-up-mouth',
|
"blendshape-corner-down-mouth.L",
|
||||||
'blendshape-center-down-mouth',
|
"blendshape-corner-up-mouth.L",
|
||||||
'hat1.R', 'hat2.R', 'hat3.R', 'hat1.L', 'hat2.L', 'hat3.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 = [
|
attributes = [
|
||||||
'location', 'rotation_quaternion',
|
"location",
|
||||||
'rotation_euler', 'rotation_axis_angle', 'scale'
|
"rotation_quaternion",
|
||||||
|
"rotation_euler",
|
||||||
|
"rotation_axis_angle",
|
||||||
|
"scale",
|
||||||
]
|
]
|
||||||
|
|
||||||
if action:
|
if action:
|
||||||
@@ -543,30 +585,40 @@ class POSELIB_OT_apply_pose_asset_for_keymap(Operator):
|
|||||||
if not prop_name in store_bones[bone_name].keys():
|
if not prop_name in store_bones[bone_name].keys():
|
||||||
store_bones[bone_name][prop_name] = []
|
store_bones[bone_name][prop_name] = []
|
||||||
|
|
||||||
val = getattr(context.object.pose.bones[bone_name], prop_name)
|
val = getattr(
|
||||||
|
context.object.pose.bones[bone_name], prop_name
|
||||||
|
)
|
||||||
|
|
||||||
store_bones[bone_name][prop_name].append(fc.evaluate(context.scene.frame_current))
|
store_bones[bone_name][prop_name].append(
|
||||||
|
fc.evaluate(context.scene.frame_current)
|
||||||
|
)
|
||||||
|
|
||||||
bpy.ops.poselib.apply_pose_asset(context.copy(), 'EXEC_DEFAULT', flipped=True)
|
bpy.ops.poselib.apply_pose_asset(
|
||||||
|
context.copy(), "EXEC_DEFAULT", flipped=True
|
||||||
|
)
|
||||||
|
|
||||||
for bone, v in store_bones.items():
|
for bone, v in store_bones.items():
|
||||||
for attr, attr_val in v.items():
|
for attr, attr_val in v.items():
|
||||||
flipped_vector = 1
|
flipped_vector = 1
|
||||||
|
|
||||||
### TODO FAIRE ÇA PROPREMENT AVEC UNE COMPREHENSION LIST OU AUTRE
|
### TODO FAIRE ÇA PROPREMENT AVEC UNE COMPREHENSION LIST OU AUTRE
|
||||||
if re.search(r'\.[RL]$', bone):
|
if re.search(r"\.[RL]$", bone):
|
||||||
flipped_bone = pose_usage.flip_side_name(bone)
|
flipped_bone = pose_usage.flip_side_name(bone)
|
||||||
if attr == 'location':
|
if attr == "location":
|
||||||
flipped_vector = Vector((-1, 1, 1))
|
flipped_vector = Vector((-1, 1, 1))
|
||||||
# print('-----', store_bones.get(flipped_bone)[attr])
|
# print('-----', store_bones.get(flipped_bone)[attr])
|
||||||
attr_val = 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)
|
setattr(context.object.pose.bones[bone], attr, attr_val)
|
||||||
|
|
||||||
return {'FINISHED'}
|
return {"FINISHED"}
|
||||||
|
|
||||||
else:
|
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):
|
class POSELIB_OT_convert_old_poselib(Operator):
|
||||||
@@ -577,12 +629,18 @@ class POSELIB_OT_convert_old_poselib(Operator):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context: Context) -> bool:
|
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:
|
if not action:
|
||||||
cls.poll_message_set("Active object has no Action")
|
cls.poll_message_set("Active object has no Action")
|
||||||
return False
|
return False
|
||||||
if not action.pose_markers:
|
if not action.pose_markers:
|
||||||
cls.poll_message_set("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 False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -593,12 +651,11 @@ class POSELIB_OT_convert_old_poselib(Operator):
|
|||||||
new_actions = conversion.convert_old_poselib(old_poselib)
|
new_actions = conversion.convert_old_poselib(old_poselib)
|
||||||
|
|
||||||
if not new_actions:
|
if not new_actions:
|
||||||
self.report({'ERROR'}, "Unable to convert to pose assets")
|
self.report({"ERROR"}, "Unable to convert to pose assets")
|
||||||
return {'CANCELLED'}
|
return {"CANCELLED"}
|
||||||
|
|
||||||
self.report({'INFO'}, "Converted %d poses to pose assets" % len(new_actions))
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
|
self.report({"INFO"}, "Converted %d poses to pose assets" % len(new_actions))
|
||||||
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
classes = (
|
classes = (
|
||||||
@@ -609,7 +666,7 @@ classes = (
|
|||||||
POSELIB_OT_create_pose_asset,
|
POSELIB_OT_create_pose_asset,
|
||||||
POSELIB_OT_paste_asset,
|
POSELIB_OT_paste_asset,
|
||||||
POSELIB_OT_pose_asset_select_bones,
|
POSELIB_OT_pose_asset_select_bones,
|
||||||
POSELIB_OT_restore_previous_action
|
POSELIB_OT_restore_previous_action,
|
||||||
)
|
)
|
||||||
|
|
||||||
register, unregister = bpy.utils.register_classes_factory(classes)
|
register, unregister = bpy.utils.register_classes_factory(classes)
|
||||||
@@ -129,7 +129,9 @@ class PoseActionCreator:
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
try:
|
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:
|
except UnresolvablePathError:
|
||||||
# A once-animated property no longer exists.
|
# A once-animated property no longer exists.
|
||||||
continue
|
continue
|
||||||
@@ -197,7 +199,9 @@ class PoseActionCreator:
|
|||||||
|
|
||||||
fcurve: Optional[FCurve] = dst_action.fcurves.find(rna_path, index=array_index)
|
fcurve: Optional[FCurve] = dst_action.fcurves.find(rna_path, index=array_index)
|
||||||
if fcurve is None:
|
if fcurve is None:
|
||||||
fcurve = dst_action.fcurves.new(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.keyframe_points.insert(self.params.src_frame_nr, value=value)
|
||||||
fcurve.update()
|
fcurve.update()
|
||||||
@@ -296,8 +300,11 @@ def create_pose_asset(
|
|||||||
pose_action.asset_generate_preview()
|
pose_action.asset_generate_preview()
|
||||||
return pose_action
|
return pose_action
|
||||||
|
|
||||||
#def create_pose_asset_from_context(context: Context, new_asset_name: str, selection=True) -> Optional[Action]:
|
|
||||||
def create_pose_asset_from_context(context: Context, new_asset_name: str) -> 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."""
|
"""Create Action asset from active object & selected bones."""
|
||||||
|
|
||||||
bones = context.selected_pose_bones_from_active_object
|
bones = context.selected_pose_bones_from_active_object
|
||||||
@@ -369,7 +376,10 @@ def copy_keyframe(dst_fcurve: FCurve, src_keyframe: Keyframe) -> Keyframe:
|
|||||||
"""Copy a keyframe from one FCurve to the other."""
|
"""Copy a keyframe from one FCurve to the other."""
|
||||||
|
|
||||||
dst_keyframe = dst_fcurve.keyframe_points.insert(
|
dst_keyframe = dst_fcurve.keyframe_points.insert(
|
||||||
src_keyframe.co.x, src_keyframe.co.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 {
|
for propname in {
|
||||||
@@ -412,7 +422,9 @@ def find_keyframe(fcurve: FCurve, frame: float) -> Optional[Keyframe]:
|
|||||||
return None
|
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.
|
"""Assign some things from the asset browser to the asset.
|
||||||
|
|
||||||
This sets the current catalog ID, and in the future could include tags
|
This sets the current catalog ID, and in the future could include tags
|
||||||
@@ -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):
|
def select_bones(arm_object: Object, action: Action, *, selected_side, toggle=True):
|
||||||
pose_bone_re = re.compile(r'pose.bones\["([^"]+)"\]')
|
pose_bone_re = re.compile(r'pose.bones\["([^"]+)"\]')
|
||||||
pose = arm_object.pose
|
pose = arm_object.pose
|
||||||
@@ -35,15 +35,14 @@ def select_bones(arm_object: Object, action: Action, *, selected_side, toggle=Tr
|
|||||||
continue
|
continue
|
||||||
seen_bone_names.add(bone_name)
|
seen_bone_names.add(bone_name)
|
||||||
|
|
||||||
if selected_side == 'FLIPPED':
|
if selected_side == "FLIPPED":
|
||||||
bones_to_select.add(bone_name_flip)
|
bones_to_select.add(bone_name_flip)
|
||||||
elif selected_side == 'BOTH':
|
elif selected_side == "BOTH":
|
||||||
bones_to_select.add(bone_name_flip)
|
bones_to_select.add(bone_name_flip)
|
||||||
bones_to_select.add(bone_name)
|
bones_to_select.add(bone_name)
|
||||||
elif selected_side == 'CURRENT':
|
elif selected_side == "CURRENT":
|
||||||
bones_to_select.add(bone_name)
|
bones_to_select.add(bone_name)
|
||||||
|
|
||||||
|
|
||||||
for bone in bones_to_select:
|
for bone in bones_to_select:
|
||||||
pose_bone = pose.bones.get(bone)
|
pose_bone = pose.bones.get(bone)
|
||||||
if pose_bone:
|
if pose_bone:
|
||||||
@@ -174,7 +173,7 @@ def flip_side_name(to_flip: str) -> str:
|
|||||||
return prefix + replace + suffix + number
|
return prefix + replace + suffix + number
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == "__main__":
|
||||||
import doctest
|
import doctest
|
||||||
|
|
||||||
print(f"Test result: {doctest.testmod()}")
|
print(f"Test result: {doctest.testmod()}")
|
||||||
+801
-31
@@ -1,59 +1,829 @@
|
|||||||
|
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
from bpy.types import AddonPreferences
|
import os
|
||||||
from bpy.props import (CollectionProperty, StringProperty)
|
from os.path import abspath, join
|
||||||
|
|
||||||
from . properties import AssetLibrary
|
from bpy.types import AddonPreferences, PointerProperty, PropertyGroup
|
||||||
from . core.bl_utils import get_addon_prefs
|
from bpy.props import (
|
||||||
from . core.lib_utils import update_library_path
|
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 pathlib import Path
|
||||||
|
import importlib
|
||||||
|
import inspect
|
||||||
|
|
||||||
|
|
||||||
|
def update_library_config(self, context):
|
||||||
|
print("update_library_config not yet implemented")
|
||||||
|
|
||||||
|
|
||||||
|
def update_library_path(self, context):
|
||||||
|
prefs = get_addon_prefs()
|
||||||
|
|
||||||
|
self["bundle_directory"] = str(self.library_path)
|
||||||
|
|
||||||
|
if not self.custom_bundle_name:
|
||||||
|
self["custom_bundle_name"] = self.name
|
||||||
|
|
||||||
|
if not self.custom_bundle_directory:
|
||||||
|
custom_bundle_dir = Path(prefs.bundle_directory, self.library_name).resolve()
|
||||||
|
self["custom_bundle_directory"] = str(custom_bundle_dir)
|
||||||
|
|
||||||
|
# if self.custom_bundle_directory:
|
||||||
|
# self['custom_bundle_directory'] = abspath(bpy.path.abspath(self.custom_bundle_directory))
|
||||||
|
# else:
|
||||||
|
# bundle_directory = join(prefs.bundle_directory, norm_str(self.name))
|
||||||
|
# self['custom_bundle_directory'] = abspath(bundle_directory)
|
||||||
|
|
||||||
|
self.set_library_path()
|
||||||
|
|
||||||
|
|
||||||
|
def update_all_library_path(self, context):
|
||||||
|
# print('update_all_assetlib_paths')
|
||||||
|
|
||||||
|
prefs = get_addon_prefs()
|
||||||
|
|
||||||
|
# if self.custom_bundle_directory:
|
||||||
|
# self['custom_bundle_directory'] = abspath(bpy.path.abspath(self.custom_bundle_directory))
|
||||||
|
|
||||||
|
for lib in prefs.libraries:
|
||||||
|
update_library_path(lib, context)
|
||||||
|
# lib.set_library_path()
|
||||||
|
|
||||||
|
|
||||||
|
def get_library_type_items(self, context):
|
||||||
|
# prefs = get_addon_prefs()
|
||||||
|
|
||||||
|
items = [("NONE", "None", "", 0)]
|
||||||
|
items += [
|
||||||
|
(norm_str(a.name, format=str.upper), a.name, "", i + 1)
|
||||||
|
for i, a in enumerate(LIBRARY_TYPES)
|
||||||
|
]
|
||||||
|
return items
|
||||||
|
|
||||||
|
|
||||||
|
def get_adapters_items(self, context):
|
||||||
|
# prefs = get_addon_prefs()
|
||||||
|
|
||||||
|
items = [("NONE", "None", "", 0)]
|
||||||
|
items += [
|
||||||
|
(norm_str(a.name, format=str.upper), a.name, "", i + 1)
|
||||||
|
for i, a in enumerate(ADAPTERS)
|
||||||
|
]
|
||||||
|
return items
|
||||||
|
|
||||||
|
|
||||||
|
def get_library_items(self, context):
|
||||||
|
prefs = get_addon_prefs()
|
||||||
|
|
||||||
|
items = [("NONE", "None", "", 0)]
|
||||||
|
items += [
|
||||||
|
(l.name, l.name, "", i + 1) for i, l in enumerate(prefs.libraries) if l != self
|
||||||
|
]
|
||||||
|
|
||||||
|
return items
|
||||||
|
|
||||||
|
|
||||||
|
def get_store_library_items(self, context):
|
||||||
|
# prefs = get_addon_prefs()
|
||||||
|
|
||||||
|
# libraries = [l for l in prefs.libraries if l.merge_library == self.name]
|
||||||
|
|
||||||
|
return [
|
||||||
|
(l.name, l.name, "", i) for i, l in enumerate([self] + self.merge_libraries)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class LibraryTypes(PropertyGroup):
|
||||||
|
def __iter__(self):
|
||||||
|
return (
|
||||||
|
getattr(self, p)
|
||||||
|
for p in self.bl_rna.properties.keys()
|
||||||
|
if p not in ("rna_type", "name")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Adapters(PropertyGroup):
|
||||||
|
def __iter__(self):
|
||||||
|
return (
|
||||||
|
getattr(self, p)
|
||||||
|
for p in self.bl_rna.properties.keys()
|
||||||
|
if p not in ("rna_type", "name")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AssetLibrary(PropertyGroup):
|
||||||
|
name: StringProperty(
|
||||||
|
name="Name", default="Action Library", update=update_library_path
|
||||||
|
)
|
||||||
|
id: StringProperty()
|
||||||
|
auto_bundle: BoolProperty(name="Auto Bundle", default=False)
|
||||||
|
expand: BoolProperty(name="Expand", default=False)
|
||||||
|
use: BoolProperty(name="Use", default=True, update=update_library_path)
|
||||||
|
data_type: EnumProperty(name="Type", items=DATA_TYPE_ITEMS, default="COLLECTION")
|
||||||
|
|
||||||
|
# template_image : StringProperty(default='', description='../{name}_image.png')
|
||||||
|
# template_video : StringProperty(default='', description='../{name}_video.mov')
|
||||||
|
# template_info : StringProperty(default='', description='../{name}_asset_info.json')
|
||||||
|
|
||||||
|
bundle_directory: StringProperty(
|
||||||
|
name="Bundle Directory", subtype="DIR_PATH", default=""
|
||||||
|
)
|
||||||
|
|
||||||
|
use_custom_bundle_directory: BoolProperty(default=False, update=update_library_path)
|
||||||
|
custom_bundle_directory: StringProperty(
|
||||||
|
name="Bundle Directory",
|
||||||
|
subtype="DIR_PATH",
|
||||||
|
default="",
|
||||||
|
update=update_library_path,
|
||||||
|
)
|
||||||
|
# use_merge : BoolProperty(default=False, update=update_library_path)
|
||||||
|
|
||||||
|
use_custom_bundle_name: BoolProperty(default=False, update=update_library_path)
|
||||||
|
custom_bundle_name: StringProperty(name="Merge Name", update=update_library_path)
|
||||||
|
# merge_library : EnumProperty(name='Merge Library', items=get_library_items, update=update_library_path)
|
||||||
|
# merge_name : StringProperty(name='Merge Name', update=update_library_path)
|
||||||
|
|
||||||
|
# Library when adding an asset to the library if merge with another
|
||||||
|
store_library: EnumProperty(items=get_store_library_items, name="Library")
|
||||||
|
|
||||||
|
template: StringProperty()
|
||||||
|
expand_extra: BoolProperty(name="Expand", default=False)
|
||||||
|
blend_depth: IntProperty(name="Blend Depth", default=1)
|
||||||
|
|
||||||
|
# source_directory : StringProperty(
|
||||||
|
# name="Path",
|
||||||
|
# subtype='DIR_PATH',
|
||||||
|
# default='',
|
||||||
|
# update=update_library_path
|
||||||
|
# )
|
||||||
|
|
||||||
|
# library_type : EnumProperty(items=library_type_ITEMS)
|
||||||
|
library_types: bpy.props.PointerProperty(type=LibraryTypes)
|
||||||
|
library_type_name: EnumProperty(items=get_library_type_items)
|
||||||
|
|
||||||
|
adapters: bpy.props.PointerProperty(type=Adapters)
|
||||||
|
adapter_name: EnumProperty(items=get_adapters_items)
|
||||||
|
|
||||||
|
parent_name: StringProperty()
|
||||||
|
|
||||||
|
# data_file_path : StringProperty(
|
||||||
|
# name="Path",
|
||||||
|
# subtype='FILE_PATH',
|
||||||
|
# default='',
|
||||||
|
# )
|
||||||
|
|
||||||
|
# def __init__(self):
|
||||||
|
# self.library_types.parent = self
|
||||||
|
|
||||||
|
@property
|
||||||
|
def parent(self):
|
||||||
|
prefs = get_addon_prefs()
|
||||||
|
if self.parent_name:
|
||||||
|
return prefs.libraries[self.parent_name]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def merge_libraries(self):
|
||||||
|
prefs = get_addon_prefs()
|
||||||
|
return [
|
||||||
|
l
|
||||||
|
for l in prefs.libraries
|
||||||
|
if l != self and (l.library_path == self.library_path)
|
||||||
|
]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def child_libraries(self):
|
||||||
|
prefs = get_addon_prefs()
|
||||||
|
return [l for l in prefs.libraries if l != self and (l.parent == self)]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def data_types(self):
|
||||||
|
data_type = self.data_type
|
||||||
|
if data_type == "FILE":
|
||||||
|
data_type = "COLLECTION"
|
||||||
|
return f"{data_type.lower()}s"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def library_type(self):
|
||||||
|
name = norm_str(self.library_type_name)
|
||||||
|
if not hasattr(self.library_types, name):
|
||||||
|
return
|
||||||
|
|
||||||
|
return getattr(self.library_types, name)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def adapter(self):
|
||||||
|
name = norm_str(self.adapter_name)
|
||||||
|
if not hasattr(self.adapters, name):
|
||||||
|
return
|
||||||
|
|
||||||
|
return getattr(self.adapters, name)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def library(self):
|
||||||
|
prefs = get_addon_prefs()
|
||||||
|
asset_lib_ref = bpy.context.space_data.params.asset_library_ref
|
||||||
|
|
||||||
|
# TODO work also outside asset_library_area
|
||||||
|
if asset_lib_ref not in prefs.libraries:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return prefs.libraries[asset_lib_ref]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def library_path(self):
|
||||||
|
prefs = get_addon_prefs()
|
||||||
|
|
||||||
|
library_name = self.library_name
|
||||||
|
# if not self.use_custom_bundle_name:
|
||||||
|
# library_name = norm_str(library_name)
|
||||||
|
|
||||||
|
if self.use_custom_bundle_directory:
|
||||||
|
return Path(self.custom_bundle_directory).resolve()
|
||||||
|
else:
|
||||||
|
library_name = norm_str(library_name)
|
||||||
|
return Path(prefs.bundle_directory, library_name).resolve()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def bundle_dir(self):
|
||||||
|
return self.library_path.as_posix()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def library_name(self):
|
||||||
|
if self.use_custom_bundle_name:
|
||||||
|
return self.custom_bundle_name
|
||||||
|
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
def read_catalog(self):
|
||||||
|
return Catalog(self.library_path).read()
|
||||||
|
|
||||||
|
def read_cache(self, filepath=None):
|
||||||
|
if filepath:
|
||||||
|
return LibraryCache(filepath).read()
|
||||||
|
|
||||||
|
return LibraryCache.from_library(self).read()
|
||||||
|
|
||||||
|
def clear_library_path(self):
|
||||||
|
# print('Clear Library Path', self.name)
|
||||||
|
|
||||||
|
prefs = bpy.context.preferences
|
||||||
|
libs = prefs.filepaths.asset_libraries
|
||||||
|
|
||||||
|
# path = self.library_path.as_posix()
|
||||||
|
|
||||||
|
for l in reversed(libs):
|
||||||
|
# lib_path = Path(l.path).resolve().as_posix()
|
||||||
|
|
||||||
|
prev_name = self.get("asset_library") or self.library_name
|
||||||
|
|
||||||
|
# print(l.name, prev_name)
|
||||||
|
|
||||||
|
if l.name == prev_name:
|
||||||
|
index = list(libs).index(l)
|
||||||
|
try:
|
||||||
|
bpy.ops.preferences.asset_library_remove(index=index)
|
||||||
|
return
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# print('No library removed')
|
||||||
|
|
||||||
|
def set_dict(self, data, obj=None):
|
||||||
|
""" "Recursive method to set all attribute from a dict to this instance"""
|
||||||
|
|
||||||
|
if obj is None:
|
||||||
|
obj = self
|
||||||
|
|
||||||
|
# Make shure the input dict is not modidied
|
||||||
|
data = data.copy()
|
||||||
|
|
||||||
|
# print(obj)
|
||||||
|
|
||||||
|
for key, value in data.items():
|
||||||
|
if isinstance(value, dict):
|
||||||
|
|
||||||
|
if "name" in value:
|
||||||
|
setattr(obj, f"{key}_name", value.pop("name"))
|
||||||
|
|
||||||
|
# print('Nested value', getattr(obj, key))
|
||||||
|
self.set_dict(value, obj=getattr(obj, key))
|
||||||
|
|
||||||
|
elif key in obj.bl_rna.properties.keys():
|
||||||
|
if key == "id":
|
||||||
|
value = str(value)
|
||||||
|
|
||||||
|
elif key == "custom_bundle_name":
|
||||||
|
if not "use_custom_bundle_name" in data.values():
|
||||||
|
obj["use_custom_bundle_name"] = True
|
||||||
|
|
||||||
|
elif isinstance(value, str):
|
||||||
|
value = os.path.expandvars(value)
|
||||||
|
value = os.path.expanduser(value)
|
||||||
|
|
||||||
|
# print('set attr', key, value)
|
||||||
|
setattr(obj, key, value)
|
||||||
|
# obj[key] = value
|
||||||
|
|
||||||
|
else:
|
||||||
|
print(f"Prop {key} of {obj} not exist")
|
||||||
|
|
||||||
|
self["bundle_directory"] = str(self.library_path)
|
||||||
|
|
||||||
|
if not self.custom_bundle_name:
|
||||||
|
self["custom_bundle_name"] = self.name
|
||||||
|
|
||||||
|
# self.library_type_name = data['library_type']
|
||||||
|
# if not self.library_type:
|
||||||
|
# print(f"No library_type named {data['library_type']}")
|
||||||
|
# return
|
||||||
|
|
||||||
|
# for key, value in data.items():
|
||||||
|
# if key == 'options':
|
||||||
|
# for k, v in data['options'].items():
|
||||||
|
# setattr(self.library_type, k, v)
|
||||||
|
# elif key in self.bl_rna.properties.keys():
|
||||||
|
# if key == 'id':
|
||||||
|
# value = str(value)
|
||||||
|
|
||||||
|
# if key == 'custom_bundle_name':
|
||||||
|
# if not 'use_custom_bundle_name' in data.values():
|
||||||
|
# self["use_custom_bundle_name"] = True
|
||||||
|
|
||||||
|
# self[key] = value
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
data = {
|
||||||
|
p: getattr(self, p)
|
||||||
|
for p in self.bl_rna.properties.keys()
|
||||||
|
if p != "rna_type"
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.library_type:
|
||||||
|
data["library_type"] = self.library_type.to_dict()
|
||||||
|
data["library_type"]["name"] = data.pop("library_type_name")
|
||||||
|
del data["library_types"]
|
||||||
|
|
||||||
|
if self.adapter:
|
||||||
|
data["adapter"] = self.adapter.to_dict()
|
||||||
|
data["adapter"]["name"] = data.pop("adapter_name")
|
||||||
|
del data["adapters"]
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
def set_library_path(self):
|
||||||
|
"""Update the Blender Preference Filepaths tab with the addon libraries"""
|
||||||
|
|
||||||
|
prefs = bpy.context.preferences
|
||||||
|
name = self.library_name
|
||||||
|
lib_path = self.library_path
|
||||||
|
|
||||||
|
self.clear_library_path()
|
||||||
|
|
||||||
|
if not self.use or not lib_path:
|
||||||
|
# if all(not l.use for l in self.merge_libraries):
|
||||||
|
# self.clear_library_path()
|
||||||
|
return
|
||||||
|
|
||||||
|
# lib = None
|
||||||
|
# if self.get('asset_library'):
|
||||||
|
# #print('old_name', self['asset_library'])
|
||||||
|
# lib = prefs.filepaths.asset_libraries.get(self['asset_library'])
|
||||||
|
|
||||||
|
# if not lib:
|
||||||
|
# #print('keys', prefs.filepaths.asset_libraries.keys())
|
||||||
|
# #print('name', name)
|
||||||
|
# #print(prefs.filepaths.asset_libraries.get(name))
|
||||||
|
# lib = prefs.filepaths.asset_libraries.get(name)
|
||||||
|
|
||||||
|
# Create the Asset Library Path
|
||||||
|
lib = prefs.filepaths.asset_libraries.get(name)
|
||||||
|
if not lib:
|
||||||
|
# print(f'Creating the lib {name}')
|
||||||
|
try:
|
||||||
|
bpy.ops.preferences.asset_library_add(directory=str(lib_path))
|
||||||
|
except AttributeError:
|
||||||
|
return
|
||||||
|
|
||||||
|
lib = prefs.filepaths.asset_libraries[-1]
|
||||||
|
|
||||||
|
lib.name = name
|
||||||
|
|
||||||
|
self["asset_library"] = name
|
||||||
|
lib.path = str(lib_path)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_user(self):
|
||||||
|
prefs = get_addon_prefs()
|
||||||
|
return self in prefs.user_libraries.values()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_env(self):
|
||||||
|
prefs = get_addon_prefs()
|
||||||
|
return self in prefs.env_libraries.values()
|
||||||
|
|
||||||
|
def add_row(
|
||||||
|
self, layout, data=None, prop=None, label="", boolean=None, factor=0.39
|
||||||
|
):
|
||||||
|
"""Act like the use_property_split but with more control"""
|
||||||
|
|
||||||
|
enabled = True
|
||||||
|
split = layout.split(factor=factor, align=True)
|
||||||
|
|
||||||
|
row = split.row(align=False)
|
||||||
|
row.use_property_split = False
|
||||||
|
row.alignment = "RIGHT"
|
||||||
|
row.label(text=str(label))
|
||||||
|
if boolean:
|
||||||
|
boolean_data = self
|
||||||
|
if isinstance(boolean, (list, tuple)):
|
||||||
|
boolean_data, boolean = boolean
|
||||||
|
|
||||||
|
row.prop(boolean_data, boolean, text="")
|
||||||
|
enabled = getattr(boolean_data, boolean)
|
||||||
|
|
||||||
|
row = split.row(align=True)
|
||||||
|
row.enabled = enabled
|
||||||
|
|
||||||
|
if isinstance(data, str):
|
||||||
|
row.label(text=data)
|
||||||
|
else:
|
||||||
|
row.prop(data or self, prop, text="")
|
||||||
|
|
||||||
|
return split
|
||||||
|
|
||||||
|
def draw_operators(self, layout):
|
||||||
|
row = layout.row(align=True)
|
||||||
|
row.alignment = "RIGHT"
|
||||||
|
row.prop(self, "library_type_name", text="")
|
||||||
|
row.prop(self, "auto_bundle", text="", icon="UV_SYNC_SELECT")
|
||||||
|
|
||||||
|
row.operator("assetlib.diff", text="", icon="FILE_REFRESH").name = self.name
|
||||||
|
|
||||||
|
op = row.operator("assetlib.bundle", icon="MOD_BUILD", text="")
|
||||||
|
op.name = self.name
|
||||||
|
|
||||||
|
layout.separator(factor=3)
|
||||||
|
|
||||||
|
def draw(self, layout):
|
||||||
|
prefs = get_addon_prefs()
|
||||||
|
# box = layout.box()
|
||||||
|
|
||||||
|
row = layout.row(align=True)
|
||||||
|
# row.use_property_split = False
|
||||||
|
|
||||||
|
# row.alignment = 'LEFT'
|
||||||
|
icon = "DISCLOSURE_TRI_DOWN" if self.expand else "DISCLOSURE_TRI_RIGHT"
|
||||||
|
row.prop(self, "expand", icon=icon, emboss=False, text="")
|
||||||
|
|
||||||
|
if self.is_user:
|
||||||
|
row.prop(self, "use", text="")
|
||||||
|
row.prop(self, "data_type", icon_only=True, emboss=False)
|
||||||
|
row.prop(self, "name", text="")
|
||||||
|
|
||||||
|
self.draw_operators(row)
|
||||||
|
|
||||||
|
index = list(prefs.user_libraries).index(self)
|
||||||
|
row.operator(
|
||||||
|
"assetlib.remove_user_library", icon="X", text="", emboss=False
|
||||||
|
).index = index
|
||||||
|
|
||||||
|
else:
|
||||||
|
row.prop(self, "use", text="")
|
||||||
|
row.label(icon=ICONS[self.data_type])
|
||||||
|
# row.label(text=self.name)
|
||||||
|
subrow = row.row(align=True)
|
||||||
|
subrow.alignment = "LEFT"
|
||||||
|
subrow.prop(self, "expand", emboss=False, text=self.name)
|
||||||
|
# row.separator_spacer()
|
||||||
|
|
||||||
|
self.draw_operators(row)
|
||||||
|
|
||||||
|
sub_row = row.row()
|
||||||
|
sub_row.enabled = False
|
||||||
|
sub_row.label(icon="FAKE_USER_ON")
|
||||||
|
|
||||||
|
if self.expand:
|
||||||
|
col = layout.column(align=False)
|
||||||
|
col.use_property_split = True
|
||||||
|
# row = col.row(align=True)
|
||||||
|
|
||||||
|
row = self.add_row(
|
||||||
|
col,
|
||||||
|
prop="custom_bundle_name",
|
||||||
|
boolean="use_custom_bundle_name",
|
||||||
|
label="Custom Bundle Name",
|
||||||
|
)
|
||||||
|
|
||||||
|
row.enabled = not self.use_custom_bundle_directory
|
||||||
|
|
||||||
|
prop = "bundle_directory"
|
||||||
|
if self.use_custom_bundle_directory:
|
||||||
|
prop = "custom_bundle_directory"
|
||||||
|
|
||||||
|
self.add_row(
|
||||||
|
col,
|
||||||
|
prop=prop,
|
||||||
|
boolean="use_custom_bundle_directory",
|
||||||
|
label="Custom Bundle Directory",
|
||||||
|
)
|
||||||
|
|
||||||
|
col.prop(self, "blend_depth")
|
||||||
|
|
||||||
|
# subcol = col.column(align=True)
|
||||||
|
# subcol.prop(self, "template_info", text='Template Info', icon='COPY_ID')
|
||||||
|
# subcol.prop(self, "template_image", text='Template Image', icon='COPY_ID')
|
||||||
|
# subcol.prop(self, "template_video", text='Template Video', icon='COPY_ID')
|
||||||
|
|
||||||
|
if self.library_type:
|
||||||
|
col.separator()
|
||||||
|
self.library_type.draw_prefs(col)
|
||||||
|
|
||||||
|
for lib in self.child_libraries:
|
||||||
|
lib.draw(layout)
|
||||||
|
|
||||||
|
col.separator()
|
||||||
|
|
||||||
|
|
||||||
|
class Collections:
|
||||||
|
"""Util Class to merge multiple collections"""
|
||||||
|
|
||||||
|
collections = []
|
||||||
|
|
||||||
|
def __init__(self, *collection):
|
||||||
|
self.collections = collection
|
||||||
|
|
||||||
|
for col in collection:
|
||||||
|
# print('Merge methods')
|
||||||
|
for attr in dir(col):
|
||||||
|
if attr.startswith("_"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
value = getattr(col, attr)
|
||||||
|
# if not callable(value):
|
||||||
|
# continue
|
||||||
|
|
||||||
|
setattr(self, attr, value)
|
||||||
|
|
||||||
|
def __contains__(self, item):
|
||||||
|
if isinstance(item, str):
|
||||||
|
return item in self.to_dict()
|
||||||
|
else:
|
||||||
|
return item in self
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
return self.to_list().__iter__()
|
||||||
|
|
||||||
|
def __getitem__(self, item):
|
||||||
|
if isinstance(item, int):
|
||||||
|
return self.to_list()[item]
|
||||||
|
else:
|
||||||
|
return self.to_dict()[item]
|
||||||
|
|
||||||
|
def get(self, item, fallback=None):
|
||||||
|
return self.to_dict().get(item) or fallback
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
return {k: v for c in self.collections for k, v in c.items()}
|
||||||
|
|
||||||
|
def to_list(self):
|
||||||
|
return [v for c in self.collections for v in c.values()]
|
||||||
|
|
||||||
|
def get_parent(self, item):
|
||||||
|
for c in self.collections:
|
||||||
|
if item in c.values():
|
||||||
|
return c
|
||||||
|
|
||||||
|
def index(self, item):
|
||||||
|
c = self.get_parent(item)
|
||||||
|
|
||||||
|
if not c:
|
||||||
|
return item in self
|
||||||
|
|
||||||
|
return list(c.values()).index(item)
|
||||||
|
|
||||||
|
|
||||||
|
# class AssetLibraryOptions(PropertyGroup):
|
||||||
|
# pass
|
||||||
|
|
||||||
|
|
||||||
class AssetLibraryPrefs(AddonPreferences):
|
class AssetLibraryPrefs(AddonPreferences):
|
||||||
bl_idname = __package__
|
bl_idname = __package__
|
||||||
|
|
||||||
config_path : StringProperty(subtype="FILE_PATH")
|
adapters = []
|
||||||
libraries : CollectionProperty(type=AssetLibrary)
|
library_types = []
|
||||||
bundle_directory : StringProperty(
|
previews = bpy.utils.previews.new()
|
||||||
name="Path",
|
preview_modal = False
|
||||||
subtype='DIR_PATH',
|
add_asset_dict = {}
|
||||||
default='',
|
|
||||||
update=lambda s, c: update_library_path()
|
# action : bpy.props.PointerProperty(type=AssetLibraryPath)
|
||||||
|
# asset : bpy.props.PointerProperty(type=AssetLibraryPath)
|
||||||
|
# library_types = {}
|
||||||
|
author: StringProperty(default=os.getlogin())
|
||||||
|
|
||||||
|
image_player: StringProperty(default="")
|
||||||
|
video_player: StringProperty(default="")
|
||||||
|
|
||||||
|
library_type_directory: StringProperty(
|
||||||
|
name="Library Type Directory", subtype="DIR_PATH"
|
||||||
)
|
)
|
||||||
|
adapter_directory: StringProperty(name="Adapter Directory", subtype="DIR_PATH")
|
||||||
|
|
||||||
|
env_libraries: CollectionProperty(type=AssetLibrary)
|
||||||
|
user_libraries: CollectionProperty(type=AssetLibrary)
|
||||||
|
expand_settings: BoolProperty(default=False)
|
||||||
|
bundle_directory: StringProperty(
|
||||||
|
name="Path", subtype="DIR_PATH", default="", update=update_all_library_path
|
||||||
|
)
|
||||||
|
|
||||||
|
config_directory: StringProperty(
|
||||||
|
name="Config Path",
|
||||||
|
subtype="FILE_PATH",
|
||||||
|
default=str(RESOURCES_DIR / "asset_library_config.json"),
|
||||||
|
update=update_library_config,
|
||||||
|
)
|
||||||
|
|
||||||
|
def load_library_types(self):
|
||||||
|
from asset_library.library_types.library_type import LibraryType
|
||||||
|
|
||||||
|
print("Asset Library: Load Library Types")
|
||||||
|
|
||||||
|
LIBRARY_TYPES.clear()
|
||||||
|
|
||||||
|
library_type_files = list(LIBRARY_TYPE_DIR.glob("*.py"))
|
||||||
|
if self.library_type_directory:
|
||||||
|
user_LIBRARY_TYPE_DIR = Path(self.library_type_directory)
|
||||||
|
if user_LIBRARY_TYPE_DIR.exists():
|
||||||
|
library_type_files += list(user_LIBRARY_TYPE_DIR.glob("*.py"))
|
||||||
|
|
||||||
|
for library_type_file in library_type_files:
|
||||||
|
if library_type_file.stem.startswith("_"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
mod = import_module_from_path(library_type_file)
|
||||||
|
|
||||||
|
# print(library_type_file)
|
||||||
|
for name, obj in inspect.getmembers(mod):
|
||||||
|
|
||||||
|
if not inspect.isclass(obj):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# print(obj.__bases__)
|
||||||
|
if not LibraryType in obj.__mro__:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Non registering base library_type
|
||||||
|
if obj is LibraryType or obj.name in (a.name for a in LIBRARY_TYPES):
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
print(f"Register Plugin {name}")
|
||||||
|
bpy.utils.register_class(obj)
|
||||||
|
setattr(
|
||||||
|
LibraryTypes,
|
||||||
|
norm_str(obj.name),
|
||||||
|
bpy.props.PointerProperty(type=obj),
|
||||||
|
)
|
||||||
|
LIBRARY_TYPES.append(obj)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Could not register library_type {name}")
|
||||||
|
print(e)
|
||||||
|
|
||||||
|
def load_adapters(self):
|
||||||
|
return
|
||||||
|
|
||||||
|
@property
|
||||||
|
def libraries(self):
|
||||||
|
return Collections(self.env_libraries, self.user_libraries)
|
||||||
|
|
||||||
def draw(self, context):
|
def draw(self, context):
|
||||||
prefs = get_addon_prefs()
|
prefs = get_addon_prefs()
|
||||||
|
|
||||||
layout = self.layout
|
layout = self.layout
|
||||||
col = layout.column(align=False)
|
# layout.use_property_split = True
|
||||||
|
|
||||||
col.prop(self, "config_path", text='Config')
|
main_col = layout.column(align=False)
|
||||||
col.prop(self, "bundle_directory", text='Bundle Directory')
|
|
||||||
col.separator()
|
|
||||||
|
|
||||||
row = col.row()
|
box = main_col.box()
|
||||||
row.label(text='Libraries:')
|
row = box.row(align=True)
|
||||||
#row.alignment = 'RIGHT'
|
icon = "DISCLOSURE_TRI_DOWN" if self.expand_settings else "DISCLOSURE_TRI_RIGHT"
|
||||||
row.separator_spacer()
|
row.prop(self, "expand_settings", icon=icon, emboss=False, text="")
|
||||||
row.operator("assetlibrary.reload_addon", icon='FILE_REFRESH', text='')
|
row.label(icon="PREFERENCES")
|
||||||
row.operator("assetlibrary.add_library", icon="ADD", text='', emboss=False)
|
row.label(text="Settings")
|
||||||
|
# row.separator_spacer()
|
||||||
|
subrow = row.row()
|
||||||
|
subrow.alignment = "RIGHT"
|
||||||
|
subrow.operator("assetlib.reload_addon", text="Reload Addon")
|
||||||
|
|
||||||
for lib in self.libraries:# list(self.env_libraries) + list(self.user_libraries):
|
if prefs.expand_settings:
|
||||||
lib.draw(col)
|
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.separator()
|
||||||
|
|
||||||
|
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.separator()
|
||||||
|
|
||||||
|
# col.prop(self, 'template_image', text='Template Image', icon='COPY_ID')
|
||||||
|
col.prop(
|
||||||
|
self, "image_player", text="Image Player"
|
||||||
|
) # icon='OUTLINER_OB_IMAGE'
|
||||||
|
|
||||||
|
# col.separator()
|
||||||
|
|
||||||
|
# col.prop(self, 'template_video', text='Template Video', icon='COPY_ID')
|
||||||
|
col.prop(self, "video_player", text="Video Player") # icon='FILE_MOVIE'
|
||||||
|
|
||||||
|
col.separator()
|
||||||
|
|
||||||
|
col.operator(
|
||||||
|
"assetlib.add_user_library",
|
||||||
|
text="Bundle All Libraries",
|
||||||
|
icon="MOD_BUILD",
|
||||||
|
)
|
||||||
|
|
||||||
|
for (
|
||||||
|
lib
|
||||||
|
) in self.libraries: # list(self.env_libraries) + list(self.user_libraries):
|
||||||
|
if lib.parent:
|
||||||
|
continue
|
||||||
|
|
||||||
|
box = main_col.box()
|
||||||
|
lib.draw(box)
|
||||||
|
|
||||||
|
row = main_col.row()
|
||||||
|
row.alignment = "RIGHT"
|
||||||
|
row.operator("assetlib.add_user_library", icon="ADD", text="", emboss=False)
|
||||||
|
|
||||||
|
|
||||||
|
classes = [
|
||||||
|
LibraryTypes,
|
||||||
classes = (
|
Adapters,
|
||||||
|
# ConformAssetLibrary,
|
||||||
|
AssetLibrary,
|
||||||
AssetLibraryPrefs,
|
AssetLibraryPrefs,
|
||||||
)
|
]
|
||||||
|
|
||||||
|
|
||||||
def register():
|
def register():
|
||||||
for cls in classes:
|
for cls in classes:
|
||||||
bpy.utils.register_class(cls)
|
bpy.utils.register_class(cls)
|
||||||
|
|
||||||
|
prefs = get_addon_prefs()
|
||||||
|
|
||||||
|
# Read Env and override preferences
|
||||||
|
bundle_dir = os.getenv("ASSETLIB_BUNDLE_DIR")
|
||||||
|
if bundle_dir:
|
||||||
|
prefs["bundle_directory"] = os.path.expandvars(bundle_dir)
|
||||||
|
|
||||||
|
config_dir = os.getenv("ASSETLIB_CONFIG_DIR")
|
||||||
|
if config_dir:
|
||||||
|
prefs["config_directory"] = os.path.expandvars(config_dir)
|
||||||
|
|
||||||
|
LIBRARY_TYPE_DIR = os.getenv("ASSETLIB_LIBRARY_TYPE_DIR")
|
||||||
|
if LIBRARY_TYPE_DIR:
|
||||||
|
prefs["library_type_directory"] = os.path.expandvars(LIBRARY_TYPE_DIR)
|
||||||
|
|
||||||
|
ADAPTER_DIR = os.getenv("ASSETLIB_ADAPTER_DIR")
|
||||||
|
if ADAPTER_DIR:
|
||||||
|
prefs["adapter_directory"] = os.path.expandvars(ADAPTER_DIR)
|
||||||
|
|
||||||
|
prefs.load_library_types()
|
||||||
|
prefs.load_adapters()
|
||||||
|
|
||||||
|
|
||||||
def unregister():
|
def unregister():
|
||||||
for cls in reversed(classes):
|
for cls in reversed(classes + LIBRARY_TYPES):
|
||||||
bpy.utils.unregister_class(cls)
|
bpy.utils.unregister_class(cls)
|
||||||
|
|
||||||
|
LIBRARY_TYPES.clear()
|
||||||
|
|||||||
-186
@@ -1,186 +0,0 @@
|
|||||||
|
|
||||||
import inspect
|
|
||||||
import os
|
|
||||||
|
|
||||||
import bpy
|
|
||||||
from bpy.types import (AddonPreferences, PropertyGroup)
|
|
||||||
from bpy.props import (BoolProperty, StringProperty, CollectionProperty,
|
|
||||||
EnumProperty, IntProperty, PointerProperty)
|
|
||||||
|
|
||||||
from .constants import PLUGINS, PLUGINS_DIR, PLUGINS_ITEMS
|
|
||||||
from .core.file_utils import import_module_from_path, norm_str
|
|
||||||
|
|
||||||
from .core.bl_utils import get_addon_prefs
|
|
||||||
from .core.lib_utils import update_library_path
|
|
||||||
|
|
||||||
|
|
||||||
def load_plugins():
|
|
||||||
from .plugins.library_plugin import LibraryPlugin
|
|
||||||
print('Asset Library: Load Library Plugins')
|
|
||||||
|
|
||||||
plugin_files = list(PLUGINS_DIR.glob('*.py'))
|
|
||||||
# if self.plugin_directory:
|
|
||||||
# user_plugin_DIR = Path(self.plugin_directory)
|
|
||||||
# if user_plugin_DIR.exists():
|
|
||||||
# plugin_files += list(user_plugin_DIR.glob('*.py'))
|
|
||||||
|
|
||||||
for plugin_file in plugin_files:
|
|
||||||
if plugin_file.stem.startswith('_'):
|
|
||||||
continue
|
|
||||||
|
|
||||||
mod = import_module_from_path(plugin_file)
|
|
||||||
|
|
||||||
for name, obj in inspect.getmembers(mod):
|
|
||||||
if not inspect.isclass(obj) or (obj is LibraryPlugin):
|
|
||||||
continue
|
|
||||||
|
|
||||||
if (LibraryPlugin not in obj.__mro__) or (obj in PLUGINS):
|
|
||||||
continue
|
|
||||||
|
|
||||||
try:
|
|
||||||
print(f'Register Plugin {name}')
|
|
||||||
bpy.utils.register_class(obj)
|
|
||||||
setattr(Plugins, norm_str(obj.name), PointerProperty(type=obj))
|
|
||||||
PLUGINS[obj.name] = obj
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f'Could not register plugin {name}')
|
|
||||||
print(e)
|
|
||||||
|
|
||||||
plugins = sorted(PLUGINS.keys())
|
|
||||||
plugin_items = [('none', 'None', '', 0)]
|
|
||||||
plugin_items += [(norm_str(p), p, "") for p in plugins]
|
|
||||||
|
|
||||||
PLUGINS_ITEMS[:] = plugin_items
|
|
||||||
|
|
||||||
return PLUGINS
|
|
||||||
|
|
||||||
|
|
||||||
class Plugins(PropertyGroup):
|
|
||||||
"""Container holding the registed library plugins"""
|
|
||||||
def __iter__(self):
|
|
||||||
return (getattr(self, p) for p in self.bl_rna.properties.keys() if p not in ('rna_type', 'name'))
|
|
||||||
|
|
||||||
|
|
||||||
class AssetLibrary(PropertyGroup):
|
|
||||||
"""Library item defining one library with his plugin and settings"""
|
|
||||||
|
|
||||||
name : StringProperty(name='Name', default='', update=lambda s, c : update_library_path())
|
|
||||||
expand : BoolProperty(name='Expand', default=False)
|
|
||||||
use : BoolProperty(name='Use', default=True, update=lambda s, c : update_library_path())
|
|
||||||
is_user : BoolProperty(default=True)
|
|
||||||
path : StringProperty(subtype='DIR_PATH', update=lambda s, c : update_library_path())
|
|
||||||
|
|
||||||
plugins : PointerProperty(type=Plugins)
|
|
||||||
plugin_name : EnumProperty(items=lambda s, c : PLUGINS_ITEMS)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def plugin(self):
|
|
||||||
return getattr(self.plugins, self.plugin_name, None)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def index(self):
|
|
||||||
prefs = get_addon_prefs()
|
|
||||||
return list(prefs.libraries).index(self)
|
|
||||||
|
|
||||||
"""
|
|
||||||
def draw_operators(self, layout):
|
|
||||||
row = layout.row(align=True)
|
|
||||||
row.alignment = 'RIGHT'
|
|
||||||
row.prop(self, 'plugin_name', text='')
|
|
||||||
row.prop(self, 'auto_bundle', text='', icon='UV_SYNC_SELECT')
|
|
||||||
|
|
||||||
row.operator("assetlibrary.diff", text='', icon='FILE_REFRESH').name = self.name
|
|
||||||
|
|
||||||
op = row.operator("assetlibrary.sync", icon='MOD_BUILD', text='')
|
|
||||||
op.name = self.name
|
|
||||||
|
|
||||||
layout.separator(factor=3)
|
|
||||||
"""
|
|
||||||
|
|
||||||
def set_dict(self, data, obj=None):
|
|
||||||
""""Recursive method to set all attribute from a dict to this instance"""
|
|
||||||
|
|
||||||
if obj is None:
|
|
||||||
obj = self
|
|
||||||
|
|
||||||
# Make shure the input dict is not modidied
|
|
||||||
data = data.copy()
|
|
||||||
|
|
||||||
for key, value in data.items():
|
|
||||||
if isinstance(value, dict):
|
|
||||||
self.set_dict(value, obj=getattr(obj, key))
|
|
||||||
|
|
||||||
elif key in obj.bl_rna.properties.keys():
|
|
||||||
|
|
||||||
if isinstance(value, str):
|
|
||||||
value = os.path.expandvars(value)
|
|
||||||
value = os.path.expanduser(value)
|
|
||||||
|
|
||||||
setattr(obj, key, value)
|
|
||||||
else:
|
|
||||||
print(f'Prop {key} of {obj} not exist')
|
|
||||||
|
|
||||||
|
|
||||||
def draw(self, layout):
|
|
||||||
prefs = get_addon_prefs()
|
|
||||||
#col = layout.column(align=True)
|
|
||||||
box = layout.box()
|
|
||||||
|
|
||||||
row = box.row(align=True)
|
|
||||||
icon = "DISCLOSURE_TRI_DOWN" if self.expand else "DISCLOSURE_TRI_RIGHT"
|
|
||||||
row.prop(self, 'expand', icon=icon, emboss=False, text='')
|
|
||||||
|
|
||||||
row.prop(self, 'use', text='')
|
|
||||||
#row.label(icon="ASSET_MANAGER")
|
|
||||||
row.prop(self, 'name', text='')
|
|
||||||
row.separator(factor=0.5)
|
|
||||||
sub = row.row()
|
|
||||||
sub.alignment = 'RIGHT'
|
|
||||||
sub.prop(self, 'plugin_name', text='')
|
|
||||||
row.separator(factor=0.5)
|
|
||||||
|
|
||||||
op = row.operator("assetlibrary.synchronize", icon='UV_SYNC_SELECT', text='')
|
|
||||||
op.name = self.name
|
|
||||||
|
|
||||||
row.separator(factor=0.5)
|
|
||||||
row.operator("assetlibrary.remove_library", icon="REMOVE", text='', emboss=False).index = self.index
|
|
||||||
|
|
||||||
#self.draw_operators(row)
|
|
||||||
if self.expand:
|
|
||||||
col = box.column(align=False)
|
|
||||||
|
|
||||||
col.use_property_split = True
|
|
||||||
col.prop(self, 'path', text='Path')
|
|
||||||
|
|
||||||
if self.plugin:
|
|
||||||
col.separator()
|
|
||||||
self.plugin.draw_prefs(col)
|
|
||||||
|
|
||||||
|
|
||||||
class WindowManagerProperties(PropertyGroup):
|
|
||||||
"""Library item defining one library with his plugin and settings"""
|
|
||||||
|
|
||||||
asset : PointerProperty(type=bpy.types.ID)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
classes = (
|
|
||||||
Plugins,
|
|
||||||
AssetLibrary,
|
|
||||||
WindowManagerProperties
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def register():
|
|
||||||
for cls in classes:
|
|
||||||
bpy.utils.register_class(cls)
|
|
||||||
|
|
||||||
bpy.types.WindowManager.asset_library = PointerProperty(type=WindowManagerProperties)
|
|
||||||
load_plugins()
|
|
||||||
|
|
||||||
def unregister():
|
|
||||||
for cls in reversed(classes):
|
|
||||||
bpy.utils.unregister_class(cls)
|
|
||||||
|
|
||||||
del bpy.types.WindowManager.asset_library
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 731 B |
@@ -1,42 +0,0 @@
|
|||||||
|
|
||||||
import argparse
|
|
||||||
import sys
|
|
||||||
import json
|
|
||||||
from pathlib import Path
|
|
||||||
import bpy
|
|
||||||
|
|
||||||
from asset_library import constants
|
|
||||||
from asset_library.core.bl_utils import load_datablocks
|
|
||||||
|
|
||||||
|
|
||||||
def publish_asset(data_type, name):
|
|
||||||
bpy.app.use_userpref_skip_save_on_exit = True
|
|
||||||
|
|
||||||
blend_file = bpy.data.filepath
|
|
||||||
preview_blend = constants.RESOURCES_DIR / 'asset_preview.blend'
|
|
||||||
col = load_datablocks(preview_blend, names='Preview', type='collections', link=False)
|
|
||||||
bpy.context.scene.collection.children.link(col)
|
|
||||||
|
|
||||||
if data_type == 'node_groups':
|
|
||||||
ntree = bpy.data.node_groups[name]
|
|
||||||
mod = bpy.data.objects['Cube'].modifiers.new(ntree_name, 'NODES')
|
|
||||||
mod.node_group = ntree
|
|
||||||
|
|
||||||
bpy.context.preferences.filepaths.save_version = 0
|
|
||||||
bpy.ops.wm.save_mainfile(compress=True, exit=True)
|
|
||||||
#bpy.ops.wm.quit_blender()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__' :
|
|
||||||
parser = argparse.ArgumentParser(description='build_collection_blends',
|
|
||||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
|
||||||
|
|
||||||
parser.add_argument('--data-type')
|
|
||||||
parser.add_argument('--datablock')
|
|
||||||
|
|
||||||
if '--' in sys.argv :
|
|
||||||
index = sys.argv.index('--')
|
|
||||||
sys.argv = [sys.argv[index-1], *sys.argv[index+1:]]
|
|
||||||
|
|
||||||
args = parser.parse_args()
|
|
||||||
publish_asset(**vars(args))
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
|
|
||||||
import argparse
|
|
||||||
import sys
|
|
||||||
import json
|
|
||||||
from pathlib import Path
|
|
||||||
import bpy
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
from asset_library import constants
|
|
||||||
from asset_library.core.bl_utils import load_datablocks
|
|
||||||
from asset_library.core.lib_utils import clear_time_tag, create_time_tag, version_file
|
|
||||||
from asset_library.core.catalog import Catalog
|
|
||||||
|
|
||||||
|
|
||||||
def get_all_datablocks():
|
|
||||||
blend_datas = [
|
|
||||||
bpy.data.collections, bpy.data.objects,
|
|
||||||
bpy.data.materials, bpy.data.node_groups, bpy.data.worlds]
|
|
||||||
|
|
||||||
return [o for blend_data in blend_datas for o in blend_data]
|
|
||||||
|
|
||||||
|
|
||||||
def publish_library_assets(library, assets, catalogs):
|
|
||||||
bpy.app.use_userpref_skip_save_on_exit = True
|
|
||||||
bpy.context.preferences.filepaths.save_version = 0
|
|
||||||
|
|
||||||
for asset, catalog_path in zip(assets, catalogs):
|
|
||||||
asset_path, asset_type, asset_name = asset.rsplit('/', 2)
|
|
||||||
|
|
||||||
#print(asset_path, asset_type, asset_name)
|
|
||||||
|
|
||||||
asset = load_datablocks(asset_path, names=asset_name, type=asset_type, link=False)
|
|
||||||
|
|
||||||
if not asset.asset_data:
|
|
||||||
asset.asset_mark()
|
|
||||||
|
|
||||||
# clear asset_mark of all other assets
|
|
||||||
for datablock in get_all_datablocks():
|
|
||||||
if datablock != asset and datablock.asset_data:
|
|
||||||
datablock.asset_clear()
|
|
||||||
|
|
||||||
if asset_type == 'node_groups':
|
|
||||||
mod = bpy.data.objects['Cube'].modifiers.new(asset.name, 'NODES')
|
|
||||||
mod.node_group = asset
|
|
||||||
|
|
||||||
elif asset_type == 'objects':
|
|
||||||
bpy.data.objects.remove(bpy.data.objects['Cube'])
|
|
||||||
bpy.data.collections['Preview'].objects.link(asset)
|
|
||||||
|
|
||||||
elif asset_type == 'materials':
|
|
||||||
bpy.data.objects['Cube'].data.materials.append(asset)
|
|
||||||
|
|
||||||
#catalog_path = asset.asset_data.catalog_simple_name.replace('-', '/')
|
|
||||||
asset_publish_path = Path(library, catalog_path, asset.name).with_suffix('.blend')
|
|
||||||
asset_publish_path.parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
# Assign or Create catalog
|
|
||||||
catalog = Catalog(library)
|
|
||||||
catalog.read()
|
|
||||||
|
|
||||||
if catalog_item := catalog.get(path=catalog_path):
|
|
||||||
catalog_id = catalog_item.id
|
|
||||||
else:
|
|
||||||
catalog_id = catalog.add(catalog_path).id
|
|
||||||
catalog.write()
|
|
||||||
|
|
||||||
asset.asset_data.catalog_id = catalog_id
|
|
||||||
|
|
||||||
# Created time tag
|
|
||||||
clear_time_tag(asset)
|
|
||||||
create_time_tag(asset)
|
|
||||||
|
|
||||||
version_file(asset_publish_path)
|
|
||||||
|
|
||||||
bpy.ops.object.make_local(type='ALL')
|
|
||||||
bpy.ops.file.make_paths_relative()
|
|
||||||
bpy.ops.wm.save_as_mainfile(filepath=str(asset_publish_path), compress=True, copy=True, relative_remap=True)
|
|
||||||
bpy.ops.wm.revert_mainfile()
|
|
||||||
|
|
||||||
bpy.ops.wm.quit_blender()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__' :
|
|
||||||
parser = argparse.ArgumentParser(description='build_collection_blends',
|
|
||||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
|
||||||
|
|
||||||
parser.add_argument('--library')
|
|
||||||
parser.add_argument('--assets', nargs='+')
|
|
||||||
parser.add_argument('--catalogs', nargs='+')
|
|
||||||
|
|
||||||
if '--' in sys.argv :
|
|
||||||
index = sys.argv.index('--')
|
|
||||||
sys.argv = [sys.argv[index-1], *sys.argv[index+1:]]
|
|
||||||
|
|
||||||
args = parser.parse_args()
|
|
||||||
publish_library_assets(**vars(args))
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
|
|
||||||
import argparse
|
|
||||||
import fnmatch
|
|
||||||
import importlib.util
|
|
||||||
import json
|
|
||||||
import re
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
# import module utils without excuting __init__
|
|
||||||
spec = importlib.util.spec_from_file_location(
|
|
||||||
"utils", Path(__file__).parent/"file_utils.py"
|
|
||||||
)
|
|
||||||
utils = importlib.util.module_from_spec(spec)
|
|
||||||
spec.loader.exec_module(utils)
|
|
||||||
|
|
||||||
|
|
||||||
def synchronize(src, dst, only_new=False, only_recent=False):
|
|
||||||
|
|
||||||
excludes=['*.sync-conflict-*', '.*']
|
|
||||||
includes=['*.blend', 'blender_assets.cats.txt']
|
|
||||||
|
|
||||||
utils.copy_dir(
|
|
||||||
src, dst,
|
|
||||||
only_new=only_new, only_recent=only_recent,
|
|
||||||
excludes=excludes, includes=includes
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__' :
|
|
||||||
parser = argparse.ArgumentParser(description='Add Comment To the tracker',
|
|
||||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
|
||||||
|
|
||||||
parser.add_argument('--src')
|
|
||||||
parser.add_argument('--dst')
|
|
||||||
parser.add_argument('--only-new', type=json.loads, default='false')
|
|
||||||
parser.add_argument('--only-recent', type=json.loads, default='false')
|
|
||||||
|
|
||||||
args = parser.parse_args()
|
|
||||||
synchronize(**vars(args))
|
|
||||||
@@ -0,0 +1,286 @@
|
|||||||
|
import bpy
|
||||||
|
from pathlib import Path
|
||||||
|
from asset_library.common.file_utils import read_file, write_file
|
||||||
|
from copy import deepcopy
|
||||||
|
import time
|
||||||
|
from itertools import groupby
|
||||||
|
|
||||||
|
|
||||||
|
class AssetCache:
|
||||||
|
def __init__(self, file_cache, data):
|
||||||
|
|
||||||
|
self.file_cache = file_cache
|
||||||
|
|
||||||
|
self._data = data
|
||||||
|
|
||||||
|
self.catalog = data["catalog"]
|
||||||
|
self.author = data.get("author", "")
|
||||||
|
self.description = data.get("description", "")
|
||||||
|
self.tags = data.get("tags", [])
|
||||||
|
self.type = data.get("type")
|
||||||
|
self.name = data["name"]
|
||||||
|
self._metadata = data.get("metadata", {})
|
||||||
|
|
||||||
|
@property
|
||||||
|
def filepath(self):
|
||||||
|
return self.file_cache.filepath
|
||||||
|
|
||||||
|
@property
|
||||||
|
def metadata(self):
|
||||||
|
metadata = {".library_id": self.library.id, ".filepath": self.filepath}
|
||||||
|
|
||||||
|
metadata.update(self.metadata)
|
||||||
|
|
||||||
|
return metadata
|
||||||
|
|
||||||
|
@property
|
||||||
|
def norm_name(self):
|
||||||
|
return self.name.replace(" ", "_").lower()
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
return dict(
|
||||||
|
catalog=self.catalog,
|
||||||
|
author=self.author,
|
||||||
|
metadata=self.metadata,
|
||||||
|
description=self.description,
|
||||||
|
tags=self.tags,
|
||||||
|
type=self.type,
|
||||||
|
name=self.name,
|
||||||
|
)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"AssetCache(name={self.name}, type={self.type}, catalog={self.catalog})"
|
||||||
|
|
||||||
|
|
||||||
|
class FileCache:
|
||||||
|
def __init__(self, library_cache, data):
|
||||||
|
|
||||||
|
self.library_cache = library_cache
|
||||||
|
self.filepath = data["filepath"]
|
||||||
|
self.modified = data.get("modified", time.time_ns())
|
||||||
|
|
||||||
|
self._data = []
|
||||||
|
|
||||||
|
for asset_cache_data in data.get("assets", []):
|
||||||
|
self.add(asset_cache_data)
|
||||||
|
|
||||||
|
def add(self, asset_cache_data):
|
||||||
|
asset_cache = AssetCache(self, asset_cache_data)
|
||||||
|
self._data.append(asset_cache)
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
return dict(
|
||||||
|
filepath=self.filepath.as_posix(),
|
||||||
|
modified=self.modified,
|
||||||
|
library_id=self.library_cache.library.id,
|
||||||
|
assets=[asset_cache.to_dict() for asset_cache in self],
|
||||||
|
)
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
return self._data.__iter__()
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
return self._data[key]
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"FileCache(filepath={self.filepath})"
|
||||||
|
|
||||||
|
|
||||||
|
class AssetCacheDiff:
|
||||||
|
def __init__(self, library_diff, asset_cache, operation):
|
||||||
|
|
||||||
|
self.library_cache = library_cache
|
||||||
|
self.filepath = data["filepath"]
|
||||||
|
self.operation = operation
|
||||||
|
|
||||||
|
|
||||||
|
class LibraryCacheDiff:
|
||||||
|
def __init__(self, filepath=None):
|
||||||
|
|
||||||
|
self.filepath = filepath
|
||||||
|
self._data = []
|
||||||
|
|
||||||
|
def add(self, asset_diff):
|
||||||
|
asset_diff = AssetCacheDiff(self, asset_diff)
|
||||||
|
self._data.append(asset_cache_diff)
|
||||||
|
|
||||||
|
def set(self, asset_diffs):
|
||||||
|
for asset_diff in asset_diffs:
|
||||||
|
self.add(asset_diff)
|
||||||
|
|
||||||
|
def read(self):
|
||||||
|
print(f"Read cache from {self.filepath}")
|
||||||
|
|
||||||
|
for asset_diff_data in read_file(self.filepath):
|
||||||
|
self.add(asset_diff_data)
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
|
def group_by(self, key):
|
||||||
|
"""Return groups of file cache diff using the key provided"""
|
||||||
|
data = list(self).sort(key=key)
|
||||||
|
return groupby(data, key=key)
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
return iter(self._data)
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
return len(self._data)
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
return self._data[key]
|
||||||
|
|
||||||
|
|
||||||
|
class LibraryCache:
|
||||||
|
|
||||||
|
def __init__(self, directory, id):
|
||||||
|
|
||||||
|
self.directory = directory
|
||||||
|
self.id = id
|
||||||
|
self._data = []
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_library(cls, library):
|
||||||
|
return cls(library.library_path, library.id)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def filename(self):
|
||||||
|
return f"blender_assets.{self.id}.json"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def filepath(self):
|
||||||
|
"""Get the filepath of the library json file relative to the library"""
|
||||||
|
return self.directory / self.filename
|
||||||
|
|
||||||
|
@property
|
||||||
|
def asset_caches(self):
|
||||||
|
"""Return an iterator to get all asset caches"""
|
||||||
|
return (asset_cache for file_cache in self for asset_cache in file_cache)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def tmp_filepath(self):
|
||||||
|
return Path(bpy.app.tempdir) / self.filename
|
||||||
|
|
||||||
|
def read(self):
|
||||||
|
print(f"Read cache from {self.filepath}")
|
||||||
|
|
||||||
|
for file_cache_data in read_file(self.filepath):
|
||||||
|
self.add(file_cache_data)
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
|
def write(self, temp=False):
|
||||||
|
filepath = self.filepath
|
||||||
|
if temp:
|
||||||
|
filepath = self.tmp_filepath
|
||||||
|
|
||||||
|
print(f"Write cache file to {filepath}")
|
||||||
|
write_file(filepath, self._data)
|
||||||
|
return filepath
|
||||||
|
|
||||||
|
def add(self, file_cache_data):
|
||||||
|
file_cache = FileCache(self, file_cache_data)
|
||||||
|
|
||||||
|
self._data.append(file_cache)
|
||||||
|
|
||||||
|
def unflatten_cache(self, cache):
|
||||||
|
"""Return a new unflattten list of asset data
|
||||||
|
grouped by filepath"""
|
||||||
|
|
||||||
|
new_cache = []
|
||||||
|
|
||||||
|
cache = deepcopy(cache)
|
||||||
|
|
||||||
|
cache.sort(key=lambda x: x["filepath"])
|
||||||
|
groups = groupby(cache, key=lambda x: x["filepath"])
|
||||||
|
|
||||||
|
keys = ["filepath", "modified", "library_id"]
|
||||||
|
|
||||||
|
for _, asset_datas in groups:
|
||||||
|
asset_datas = list(asset_datas)
|
||||||
|
|
||||||
|
# print(asset_datas[0])
|
||||||
|
|
||||||
|
asset_info = {k: asset_datas[0][k] for k in keys}
|
||||||
|
asset_info["assets"] = [
|
||||||
|
{k: v for k, v in a.items() if k not in keys + ["operation"]}
|
||||||
|
for a in asset_datas
|
||||||
|
]
|
||||||
|
|
||||||
|
new_cache.append(asset_info)
|
||||||
|
|
||||||
|
return new_cache
|
||||||
|
|
||||||
|
def diff(self, new_cache):
|
||||||
|
"""Compare the library cache with it current state and return the cache differential"""
|
||||||
|
|
||||||
|
cache = self.read()
|
||||||
|
|
||||||
|
cache_dict = {f"{a['filepath']}/{a['name']}": a for a in cache.asset_caches}
|
||||||
|
new_cache_dict = {
|
||||||
|
f"{a['filepath']}/{a['name']}": a for a in new_cache.asset_caches
|
||||||
|
}
|
||||||
|
|
||||||
|
assets_added = [
|
||||||
|
AssetCacheDiff(v, "ADD") for k, v in new_cache.items() if k not in cache
|
||||||
|
]
|
||||||
|
assets_removed = [
|
||||||
|
AssetCacheDiff(v, "REMOVED") for k, v in cache.items() if k not in new_cache
|
||||||
|
]
|
||||||
|
assets_modified = [
|
||||||
|
AssetCacheDiff(v, "MODIFIED")
|
||||||
|
for k, v in cache.items()
|
||||||
|
if v not in assets_removed and v != new_cache[k]
|
||||||
|
]
|
||||||
|
|
||||||
|
if assets_added:
|
||||||
|
print(
|
||||||
|
f"{len(assets_added)} Assets Added \n{tuple(a.name for a in assets_added[:10])}...\n"
|
||||||
|
)
|
||||||
|
if assets_removed:
|
||||||
|
print(
|
||||||
|
f"{len(assets_removed)} Assets Removed \n{tuple(a.name for a in assets_removed[:10])}...\n"
|
||||||
|
)
|
||||||
|
if assets_modified:
|
||||||
|
print(
|
||||||
|
f"{len(assets_modified)} Assets Modified \n{tuple(a.name for a in assets_modified[:10])}...\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
cache_diff = LibraryCacheDiff()
|
||||||
|
cache_diff.set(assets_added + assets_removed + assets_modified)
|
||||||
|
|
||||||
|
if not len(LibraryCacheDiff):
|
||||||
|
print("No change in the library")
|
||||||
|
|
||||||
|
return cache_diff
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
return len(self._data)
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
return iter(self._data)
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
return self._data[key]
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"LibraryCache(library={self.library.name})"
|
||||||
|
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
prefs = bpy.context.preferences.addons["asset_library"].preferences
|
||||||
|
|
||||||
|
|
||||||
|
library = prefs.env_libraries[0]
|
||||||
|
library_cache = LibraryCache.from_library(library).read()
|
||||||
|
|
||||||
|
data = library.library_type.fetch()
|
||||||
|
print(data)
|
||||||
|
|
||||||
|
print(library_cache[0][0])
|
||||||
|
|
||||||
|
# library_cache.diff(library.library_type.fetch())
|
||||||
|
|
||||||
|
|
||||||
|
# print(library_cache[0])
|
||||||
@@ -1,125 +0,0 @@
|
|||||||
|
|
||||||
import bpy
|
|
||||||
from bpy.types import Menu
|
|
||||||
|
|
||||||
from .core.lib_utils import get_active_library
|
|
||||||
|
|
||||||
|
|
||||||
class ASSETLIB_MT_node_editor(Menu):
|
|
||||||
bl_label = "Asset"
|
|
||||||
|
|
||||||
def draw(self, context):
|
|
||||||
layout = self.layout
|
|
||||||
op = layout.operator("assetlibrary.publish_asset", text='Publish Node Group', icon='NODETREE')
|
|
||||||
op.data_type = 'NodeTree'
|
|
||||||
|
|
||||||
if context.space_data.tree_type == 'GeometryNodeTree':
|
|
||||||
op = layout.operator("assetlibrary.publish_asset", text='Publish Object', icon='OBJECT_DATA')
|
|
||||||
op.data_type = 'Object'
|
|
||||||
|
|
||||||
elif context.space_data.tree_type == 'ShaderNodeTree':
|
|
||||||
op = layout.operator("assetlibrary.publish_asset", text='Publish Material', icon='MATERIAL')
|
|
||||||
op.data_type = 'Material'
|
|
||||||
|
|
||||||
|
|
||||||
def draw_assetbrowser_header(self, context):
|
|
||||||
lib = get_active_library()
|
|
||||||
|
|
||||||
if not lib:
|
|
||||||
FILEBROWSER_HT_header._draw_asset_browser_buttons(self, context)
|
|
||||||
return
|
|
||||||
|
|
||||||
space_data = context.space_data
|
|
||||||
params = context.space_data.params
|
|
||||||
|
|
||||||
row = self.layout.row(align=True)
|
|
||||||
row.separator()
|
|
||||||
|
|
||||||
row.operator("assetlibrary.bundle", icon='UV_SYNC_SELECT', text='').name = lib.name
|
|
||||||
#op
|
|
||||||
#op.clean = False
|
|
||||||
#op.only_recent = True
|
|
||||||
|
|
||||||
lib.plugin.draw_header(row)
|
|
||||||
|
|
||||||
if context.selected_files and context.active_file:
|
|
||||||
row.separator()
|
|
||||||
row.label(text=context.active_file.name)
|
|
||||||
|
|
||||||
row.separator_spacer()
|
|
||||||
|
|
||||||
sub = row.row()
|
|
||||||
sub.ui_units_x = 10
|
|
||||||
sub.prop(params, "filter_search", text="", icon='VIEWZOOM')
|
|
||||||
|
|
||||||
row.separator_spacer()
|
|
||||||
|
|
||||||
row.prop_with_popover(
|
|
||||||
params,
|
|
||||||
"display_type",
|
|
||||||
panel="ASSETBROWSER_PT_display",
|
|
||||||
text="",
|
|
||||||
icon_only=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
row.operator(
|
|
||||||
"screen.region_toggle",
|
|
||||||
text="",
|
|
||||||
icon='PREFERENCES',
|
|
||||||
depress=is_option_region_visible(context, space_data)
|
|
||||||
).region_type = 'TOOL_PROPS'
|
|
||||||
|
|
||||||
|
|
||||||
def draw_assetbrowser_asset_menu(self, context):
|
|
||||||
layout = self.layout
|
|
||||||
layout.operator("assetlibrary.publish_assets", text='Publish Assets', icon='ASSET_MANAGER')
|
|
||||||
|
|
||||||
# if not get_active_library():
|
|
||||||
# return
|
|
||||||
|
|
||||||
# self.layout.separator()
|
|
||||||
# box = self.layout.box()
|
|
||||||
# row = box.row()
|
|
||||||
# row.separator(factor=0.5)
|
|
||||||
# row.label(text='Asset Library')
|
|
||||||
# row.separator(factor=0.5)
|
|
||||||
|
|
||||||
# classes = (,
|
|
||||||
# # ASSETLIB_PT_pose_library_editing,
|
|
||||||
# # ASSETLIB_PT_pose_library_usage,
|
|
||||||
# # ASSETLIB_MT_context_menu,
|
|
||||||
# # ASSETLIB_PT_libraries
|
|
||||||
# )
|
|
||||||
|
|
||||||
def draw_asset_preview_menu(self, context):
|
|
||||||
layout = self.layout
|
|
||||||
layout.operator("assetlibrary.save_asset_preview")
|
|
||||||
|
|
||||||
|
|
||||||
def draw_node_tree_menu(self, context):
|
|
||||||
layout = self.layout
|
|
||||||
row = layout.row(align=False)
|
|
||||||
row.menu('ASSETLIB_MT_node_editor')
|
|
||||||
|
|
||||||
|
|
||||||
bl_classes = (
|
|
||||||
ASSETLIB_MT_node_editor,)
|
|
||||||
|
|
||||||
|
|
||||||
def register() -> None:
|
|
||||||
for bl_class in bl_classes:
|
|
||||||
bpy.utils.register_class(bl_class)
|
|
||||||
|
|
||||||
#bpy.types.ASSETBROWSER_MT_editor_menus.append(draw_assetbrowser_header)
|
|
||||||
bpy.types.ASSETBROWSER_MT_metadata_preview_menu.append(draw_asset_preview_menu)
|
|
||||||
bpy.types.NODE_MT_editor_menus.append(draw_node_tree_menu)
|
|
||||||
|
|
||||||
bpy.types.ASSETBROWSER_MT_asset.append(draw_assetbrowser_asset_menu)
|
|
||||||
|
|
||||||
def unregister() -> None:
|
|
||||||
for bl_class in reversed(bl_classes):
|
|
||||||
bpy.utils.unregister_class(bl_class)
|
|
||||||
|
|
||||||
bpy.types.ASSETBROWSER_MT_editor_menus.remove(draw_assetbrowser_header)
|
|
||||||
bpy.types.NODE_MT_editor_menus.remove(draw_node_tree_menu)
|
|
||||||
bpy.types.ASSETBROWSER_MT_asset.remove(draw_assetbrowser_asset_menu)
|
|
||||||
Reference in New Issue
Block a user