1220 lines
39 KiB
Python
1220 lines
39 KiB
Python
# SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
|
|
"""
|
|
Pose Library - operators.
|
|
"""
|
|
|
|
from math import radians
|
|
from mathutils import Vector
|
|
from pathlib import Path
|
|
from tempfile import gettempdir
|
|
from typing import Optional, Set
|
|
|
|
import bpy
|
|
import json
|
|
import os
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
import uuid
|
|
import time
|
|
from pathlib import Path
|
|
from functools import partial
|
|
from pprint import pprint
|
|
|
|
|
|
from asset_library.pose.pose_creation import (
|
|
create_pose_asset_from_context,
|
|
assign_from_asset_browser,
|
|
)
|
|
|
|
from asset_library.pose.pose_usage import select_bones, flip_side_name
|
|
|
|
from asset_library.action.functions import (
|
|
apply_anim,
|
|
append_action,
|
|
clean_action,
|
|
reset_bone,
|
|
is_asset_action,
|
|
conform_action,
|
|
)
|
|
|
|
from bpy.props import (
|
|
BoolProperty,
|
|
CollectionProperty,
|
|
EnumProperty,
|
|
PointerProperty,
|
|
StringProperty,
|
|
IntProperty,
|
|
)
|
|
|
|
from bpy.types import (
|
|
Action,
|
|
Context,
|
|
Event,
|
|
FileSelectEntry,
|
|
Object,
|
|
Operator,
|
|
PropertyGroup,
|
|
)
|
|
|
|
from bpy_extras import asset_utils
|
|
from bpy_extras.io_utils import ExportHelper, ImportHelper
|
|
|
|
from asset_library.action.functions import (
|
|
is_pose,
|
|
get_marker,
|
|
get_keyframes,
|
|
)
|
|
|
|
from asset_library.common.functions import (
|
|
# get_actionlib_dir,
|
|
# get_asset_source,
|
|
# get_catalog_path,
|
|
# read_catalog,
|
|
# set_actionlib_dir,
|
|
# resync_lib,
|
|
get_active_library,
|
|
get_active_catalog,
|
|
asset_warning_callback,
|
|
)
|
|
|
|
from asset_library.common.bl_utils import (
|
|
area_from_context,
|
|
attr_set,
|
|
get_addon_prefs,
|
|
copy_frames,
|
|
split_path,
|
|
get_preview,
|
|
get_view3d_persp,
|
|
get_viewport,
|
|
# load_assets_from,
|
|
get_asset_space_params,
|
|
get_bl_cmd,
|
|
get_overriden_col,
|
|
)
|
|
|
|
from asset_library.common.file_utils import (
|
|
remove_version,
|
|
synchronize,
|
|
open_file,
|
|
copy_dir,
|
|
)
|
|
|
|
|
|
class ACTIONLIB_OT_restore_previous_action(Operator):
|
|
bl_idname = "actionlib.restore_previous_action"
|
|
bl_label = "Restore Previous Action"
|
|
bl_description = "Switch back to the previous Action, after creating a pose asset"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
|
|
@classmethod
|
|
def poll(cls, context: Context) -> bool:
|
|
return bool(
|
|
context.scene.actionlib.previous_action
|
|
and context.object
|
|
and context.object.animation_data
|
|
and context.object.animation_data.action
|
|
and context.object.animation_data.action.asset_data is not None
|
|
)
|
|
|
|
def execute(self, context: Context) -> Set[str]:
|
|
# This is the Action that was just created with "Create Pose Asset".
|
|
# It has to be re-applied after switching to the previous action,
|
|
# to ensure the character keeps the same pose.
|
|
self.pose_action = context.object.animation_data.action
|
|
|
|
prev_action = context.scene.actionlib.previous_action
|
|
context.object.animation_data.action = prev_action
|
|
context.scene.actionlib.previous_action = None
|
|
|
|
# Wait a bit for the action assignment to be handled, before applying the pose.
|
|
wm = context.window_manager
|
|
self._timer = wm.event_timer_add(0.001, window=context.window)
|
|
wm.modal_handler_add(self)
|
|
|
|
return {"RUNNING_MODAL"}
|
|
|
|
def modal(self, context, event):
|
|
if event.type != "TIMER":
|
|
return {"RUNNING_MODAL"}
|
|
|
|
wm = context.window_manager
|
|
wm.event_timer_remove(self._timer)
|
|
|
|
context.object.pose.apply_pose_from_action(self.pose_action)
|
|
return {"FINISHED"}
|
|
|
|
|
|
class ACTIONLIB_OT_assign_action(Operator):
|
|
bl_idname = "actionlib.assign_action"
|
|
bl_label = "Assign Action"
|
|
bl_description = "Set this pose Action as active Action on the active Object"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
|
|
assign: BoolProperty(name="Assign", default=True) # type: ignore
|
|
|
|
def execute(self, context: Context) -> Set[str]:
|
|
if self.assign:
|
|
context.object.animation_data_create().action = context.id
|
|
else:
|
|
if context.object.animation_data.action:
|
|
context.object.animation_data.action = None
|
|
|
|
return {"FINISHED"}
|
|
|
|
|
|
class ACTIONLIB_OT_replace_pose(Operator):
|
|
bl_idname = "actionlib.replace_pose"
|
|
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
|
bl_label = "Update Pose"
|
|
bl_description = "Update selected Pose. ! Works only on Pose, not Anim !"
|
|
|
|
@classmethod
|
|
def poll(cls, context: Context) -> bool:
|
|
# wm = context.window_manager
|
|
# if context.active_file and wm.edit_pose:
|
|
# if context.active_file.name == wm.edit_pose_action:
|
|
# return True
|
|
# else:
|
|
# cls.poll_message_set(f"Current Action {context.id.name} different than Edit Action {wm.edit_pose_action}")
|
|
# return False
|
|
if (
|
|
context.mode == "POSE"
|
|
and context.area.ui_type == "ASSETS"
|
|
and context.active_file
|
|
):
|
|
return True
|
|
|
|
def execute(self, context: Context) -> Set[str]:
|
|
wm = context.window_manager
|
|
active = context.active_file
|
|
# print('active: ', active)
|
|
|
|
select_bones(
|
|
context.object,
|
|
context.asset_file_handle.local_id,
|
|
selected_side="BOTH",
|
|
toggle=False,
|
|
)
|
|
|
|
data = {
|
|
"name": active.name,
|
|
"catalog_id": active.asset_data.catalog_id,
|
|
}
|
|
data.update(dict(active.asset_data))
|
|
|
|
# if 'camera' in asset_data.keys():
|
|
# data.update({'camera':active.asset_data['camera']})
|
|
# if 'is_single_frame' in asset_data.keys():
|
|
# data.update({'is_single_frame' : active.asset_data['is_single_frame']})
|
|
|
|
# print('data: ', data)
|
|
|
|
action = create_pose_asset_from_context(
|
|
context,
|
|
data["name"],
|
|
)
|
|
if not action:
|
|
self.report( # type: ignore
|
|
{"ERROR"},
|
|
f"Can't Update Current Pose",
|
|
)
|
|
return {"CANCELLED"}
|
|
|
|
bpy.ops.asset.clear()
|
|
_old_action = bpy.data.actions[active.name]
|
|
_old_action.use_fake_user = False
|
|
bpy.data.actions.remove(_old_action)
|
|
|
|
action.name = data["name"]
|
|
action.asset_data.catalog_id = data["catalog_id"]
|
|
|
|
for k, v in data.items():
|
|
if k in ("camera", "is_single_frame"):
|
|
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()
|
|
):
|
|
for tag in action.asset_data.tags:
|
|
if tag != "pose":
|
|
continue
|
|
action.asset_data.tags.remove(tag)
|
|
action.asset_data.tags.new("anim")
|
|
|
|
return {"FINISHED"}
|
|
|
|
|
|
class ACTIONLIB_OT_apply_anim(Operator):
|
|
bl_idname = "actionlib.apply_anim"
|
|
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
|
bl_label = "Apply Anim"
|
|
bl_description = "Apply selected Anim to selected bones"
|
|
|
|
@classmethod
|
|
def poll(cls, context: Context) -> bool:
|
|
return context.mode == "POSE"
|
|
|
|
def execute(self, context: Context) -> Set[str]:
|
|
ob = context.object
|
|
active_action = context.active_file
|
|
to_remove = False
|
|
|
|
prefs = get_addon_prefs()
|
|
# params = get_asset_space_params(context.area)
|
|
asset_library_ref = context.asset_library_ref
|
|
|
|
if asset_library_ref == "LOCAL":
|
|
action = bpy.data.actions[active_action.name]
|
|
to_remove = False
|
|
else:
|
|
asset_file_handle = bpy.context.asset_file_handle
|
|
if asset_file_handle is None:
|
|
return {"CANCELLED"}
|
|
|
|
if asset_file_handle.local_id:
|
|
return {"CANCELLED"}
|
|
|
|
lib = get_active_library()
|
|
if "filepath" in asset_file_handle.asset_data:
|
|
action_path = asset_file_handle.asset_data["filepath"]
|
|
action_path = lib.library_type.format_path(action_path)
|
|
else:
|
|
action_path = bpy.types.AssetHandle.get_full_library_path(
|
|
asset_file_handle, asset_library_ref
|
|
)
|
|
|
|
if action_path and Path(action_path).exists():
|
|
action = append_action(
|
|
action_path=action_path,
|
|
action_name=active_action.name,
|
|
)
|
|
to_remove = True
|
|
else:
|
|
self.report(
|
|
{"WARNING"}, f"Could not load action path {action_path} not exist"
|
|
)
|
|
return {"CANCELLED"}
|
|
|
|
bones = [b.name for b in context.selected_pose_bones_from_active_object]
|
|
apply_anim(action, context.active_object, bones=bones)
|
|
if to_remove:
|
|
bpy.data.actions.remove(action)
|
|
|
|
return {"FINISHED"}
|
|
|
|
|
|
"""
|
|
class ACTIONLIB_OT_publish(Operator):
|
|
bl_idname = "actionlib.publish"
|
|
bl_label = "Publish Library"
|
|
bl_description = "Publish Pose Lib"
|
|
|
|
#ExportHelper mixin class uses this
|
|
filename_ext = ""
|
|
use_filter_folder = True
|
|
check_extension = None
|
|
# filter_glob = StringProperty(
|
|
# default="*.blend",
|
|
# )
|
|
|
|
filepath: StringProperty(
|
|
name="File Path",
|
|
description="Filepath used for exporting the file",
|
|
maxlen=1024,
|
|
subtype='DIR_PATH',
|
|
)
|
|
selected_actions : CollectionProperty(type=PropertyGroup)
|
|
render_preview : BoolProperty(default=False)
|
|
clean : BoolProperty(default=False)
|
|
|
|
def invoke(self, context, _event):
|
|
scn = context.scene
|
|
|
|
filename = remove_version(bpy.data.filepath)
|
|
filename = re.sub('_actionlib', '', filename)
|
|
|
|
if scn.actionlib.publish_path:
|
|
action_dir = Path(os.path.expandvars(scn.actionlib.publish_path))
|
|
if action_dir.is_file():
|
|
action_dir = action_dir.parent
|
|
self.filepath = str(action_dir)
|
|
else:
|
|
self.filepath = str(Path(get_actionlib_dir()) / Path(filename).stem)
|
|
|
|
context.window_manager.fileselect_add(self)
|
|
return {'RUNNING_MODAL'}
|
|
|
|
def execute(self, context: Context) -> Set[str]:
|
|
scn = context.scene
|
|
scn.actionlib.publish_path = self.filepath.replace(
|
|
get_actionlib_dir(), '$ACTIONLIB_DIR'
|
|
)
|
|
|
|
action_dir = Path(self.filepath)
|
|
|
|
try:
|
|
bpy.ops.asset.catalogs_save()
|
|
except RuntimeError:
|
|
pass
|
|
|
|
actionlib_dir = get_actionlib_dir()
|
|
|
|
current_catalog = get_catalog_path()
|
|
current_catalog_data = read_catalog(current_catalog)
|
|
|
|
catalog_global = get_catalog_path(actionlib_dir)
|
|
catalog_data = read_catalog(catalog_global)
|
|
|
|
for d in catalog_global.parent.rglob('*'):
|
|
if not d.is_dir():
|
|
continue
|
|
|
|
reldir = d.relative_to(actionlib_dir)
|
|
|
|
if reldir not in catalog_data:
|
|
catalog_data[reldir.as_posix()] = {
|
|
'id':str(uuid.uuid4()),
|
|
'name':'-'.join(reldir.parts)
|
|
}
|
|
|
|
catalog_path = action_dir.relative_to(actionlib_dir)
|
|
for k, v in current_catalog_data.items():
|
|
v['name'] = '-'.join([*catalog_path.parts, v['name']])
|
|
catalog_data[(catalog_path / k).as_posix()] = v
|
|
|
|
catalog_global_lines = ['VERSION 1', '']
|
|
for k, v in sorted(catalog_data.items()):
|
|
if any(i in k for i in ('anim', 'pose', 'preview')):
|
|
continue
|
|
catalog_global_lines.append(':'.join([v['id'], k, v['name']]))
|
|
|
|
catalog_global.write_text('\n'.join(catalog_global_lines))
|
|
|
|
if not self.selected_actions:
|
|
render_actions_name = [a.name for a in bpy.data.actions if is_asset_action(a)]
|
|
else:
|
|
render_actions_name = [a.name for a in self.selected_actions]
|
|
|
|
publish_actions_name = []
|
|
publish_actions = {}
|
|
for a in bpy.data.actions:
|
|
if is_asset_action(a):
|
|
if not a.asset_data.catalog_id in publish_actions:
|
|
publish_actions[a.asset_data.catalog_id] = set()
|
|
|
|
publish_actions[a.asset_data.catalog_id].add(a)
|
|
publish_actions_name.append(a.name)
|
|
|
|
for cat_id, actions in publish_actions.items():
|
|
# Cleanup actions
|
|
store_actions = actions.copy()
|
|
for a in actions:
|
|
conform_action(a)
|
|
|
|
action_rel_path = next(k for k, v in current_catalog_data.items() if v['id']==cat_id)
|
|
action_path = action_dir / action_rel_path
|
|
action_path = action_path.parent / f'{action_dir.name}_{action_path.name}.blend'
|
|
|
|
action_path.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
print(f'Saving File to: {action_path}')
|
|
bpy.data.libraries.write(str(action_path), store_actions, compress=True)
|
|
|
|
# Render Previews
|
|
_tmp_filepath = Path(gettempdir()) / Path(bpy.data.filepath).name
|
|
print('_tmp_filepath: ', _tmp_filepath)
|
|
|
|
bpy.ops.wm.save_as_mainfile(filepath=str(_tmp_filepath), copy=True, compress=True)
|
|
|
|
if self.render_preview:
|
|
print('[>-] Rendering Preview..')
|
|
cmd = [
|
|
bpy.app.binary_path,
|
|
'--no-window-focus',
|
|
'--window-geometry', '5000', '0', '10', '10',
|
|
str(_tmp_filepath),
|
|
'--python-use-system-env',
|
|
'--python', str(Path(__file__).parent / 'render_preview.py'),
|
|
'--',
|
|
'--directory', str(action_dir),
|
|
'--asset-catalog', str(current_catalog),
|
|
'--render-actions', *render_actions_name,
|
|
'--publish-actions', *publish_actions_name,
|
|
'--remove-folder', json.dumps(self.clean)
|
|
]
|
|
|
|
print('cmd: ', cmd)
|
|
subprocess.Popen(cmd)
|
|
|
|
print('[>-] Publish Done.')
|
|
return {"FINISHED"}
|
|
"""
|
|
|
|
|
|
class ACTIONLIB_OT_create_anim_asset(Operator):
|
|
bl_idname = "actionlib.create_anim_asset"
|
|
bl_label = "Create Anim Asset"
|
|
bl_description = (
|
|
"Create a new Action that contains the anim of the selected bones, and mark it as Asset. "
|
|
"The asset will be stored in the current blend file"
|
|
)
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
|
|
@classmethod
|
|
def poll(cls, context: Context) -> bool:
|
|
if not context.object:
|
|
return
|
|
if not context.object.animation_data:
|
|
cls.poll_message_set("Object has no Keyframes")
|
|
return
|
|
if not context.object.animation_data.action:
|
|
cls.poll_message_set("Object has no Action")
|
|
return
|
|
|
|
# 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] = area_from_context(context)
|
|
if not asset_browse_area:
|
|
# No asset browser is visible, so there also aren't any expectations
|
|
# that this asset will be visible.
|
|
return True
|
|
|
|
params = get_asset_space_params(asset_browse_area)
|
|
if params.asset_library_ref != "LOCAL":
|
|
cls.poll_message_set(
|
|
"Asset Browser must be set to the Current File library"
|
|
)
|
|
return False
|
|
|
|
return True
|
|
|
|
def execute(self, context: Context) -> Set[str]:
|
|
### ADD ADM
|
|
action = context.object.animation_data.action
|
|
action.asset_mark()
|
|
action.asset_generate_preview()
|
|
|
|
data = action.asset_data
|
|
# data.catalog_id = str(uuid.UUID(int=0))
|
|
asset_browse_area: Optional[bpy.types.Area] = area_from_context(context)
|
|
asset_space_params = params(asset_browse_area)
|
|
|
|
data.catalog_id = asset_space_params.catalog_id
|
|
|
|
data_dict = dict(
|
|
is_single_frame=False,
|
|
camera=context.scene.camera.name if context.scene.camera else "",
|
|
)
|
|
data.tags.new("anim")
|
|
|
|
for k, v in data_dict.items():
|
|
data[k] = v
|
|
###
|
|
|
|
return {"FINISHED"}
|
|
|
|
|
|
class ACTIONLIB_OT_apply_selected_action(Operator):
|
|
bl_idname = "actionlib.apply_selected_action"
|
|
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
|
bl_label = "Apply Pose/Anim"
|
|
bl_description = "Apply selected Action to selected bones"
|
|
|
|
flipped: BoolProperty(name="Flipped", default=False) # type: ignore
|
|
|
|
@classmethod
|
|
def poll(cls, context: Context) -> bool:
|
|
if context.mode == "POSE" and context.area.ui_type == "ASSETS":
|
|
return True
|
|
|
|
def execute(self, context: Context) -> Set[str]:
|
|
active_action = context.active_file
|
|
|
|
if "pose" in active_action.asset_data.tags.keys():
|
|
bpy.ops.poselib.apply_pose_asset_for_keymap(flipped=self.flipped)
|
|
else:
|
|
bpy.ops.actionlib.apply_anim()
|
|
|
|
return {"FINISHED"}
|
|
|
|
|
|
class ACTIONLIB_OT_edit_action(Operator):
|
|
bl_idname = "actionlib.edit_action"
|
|
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
|
bl_label = "Edit Action"
|
|
bl_description = "Assign active action and set Camera"
|
|
|
|
@classmethod
|
|
def poll(cls, context: Context) -> bool:
|
|
if (
|
|
context.mode == "POSE"
|
|
and context.area.ui_type == "ASSETS"
|
|
and context.active_file
|
|
):
|
|
return True
|
|
|
|
def execute(self, context: Context) -> Set[str]:
|
|
scn = context.scene
|
|
rest_pose = bpy.data.actions.get(context.id.asset_data.get("rest_pose", ""))
|
|
|
|
context.object.animation_data_create()
|
|
keyframes = get_keyframes(context.id)
|
|
if not keyframes:
|
|
self.report({"ERROR"}, f"No Keyframes found for {context.id.name}.")
|
|
return
|
|
scn.frame_set(keyframes[0])
|
|
|
|
context.object.animation_data.action = None
|
|
|
|
for b in context.object.pose.bones:
|
|
if re.match("^[A-Z]+\.", b.name):
|
|
continue
|
|
reset_bone(b)
|
|
|
|
context.object.animation_data.action = rest_pose
|
|
context.view_layer.update()
|
|
context.object.animation_data.action = context.id
|
|
|
|
if "camera" in context.id.asset_data.keys():
|
|
scn.camera = bpy.data.objects[context.id.asset_data["camera"]]
|
|
|
|
return {"FINISHED"}
|
|
|
|
|
|
class ACTIONLIB_OT_clear_action(Operator):
|
|
bl_idname = "actionlib.clear_action"
|
|
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
|
bl_label = "Clear Action"
|
|
bl_description = "Assign active action and set Camera"
|
|
|
|
@classmethod
|
|
def poll(cls, context: Context) -> bool:
|
|
if (
|
|
context.mode == "POSE"
|
|
and context.area.ui_type == "ASSETS"
|
|
and context.active_file
|
|
):
|
|
return True
|
|
|
|
def execute(self, context: Context) -> Set[str]:
|
|
wm = context.window_manager
|
|
|
|
context.object.animation_data_create()
|
|
context.object.animation_data.action = None
|
|
context.id.asset_generate_preview()
|
|
|
|
for a in context.screen.areas:
|
|
if a.type == "DOPESHEET_EDITOR" and a.ui_type == "DOPESHEET":
|
|
a.tag_redraw()
|
|
|
|
return {"FINISHED"}
|
|
|
|
|
|
class ACTIONLIB_OT_generate_preview(Operator):
|
|
bl_idname = "actionlib.generate_preview"
|
|
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
|
bl_label = "Generate Preview"
|
|
bl_description = "Genreate Preview for Active Action"
|
|
|
|
@classmethod
|
|
def poll(cls, context: Context) -> bool:
|
|
if context.object.type == "ARMATURE" and context.mode == "POSE":
|
|
return True
|
|
|
|
def execute(self, context: Context) -> Set[str]:
|
|
_action = context.object.animation_data.action
|
|
_camera = context.scene.camera
|
|
|
|
context.object.animation_data_create()
|
|
actions = context.selected_files + [_action]
|
|
|
|
for a in context.selected_files:
|
|
rest_pose = bpy.data.actions.get(a.asset_data.get("rest_pose", ""))
|
|
if rest_pose:
|
|
context.object.animation_data.action = rest_pose
|
|
bpy.context.view_layer.update()
|
|
else:
|
|
context.object.animation_data.action = None
|
|
for b in context.object.pose.bones:
|
|
if re.match("^[A-Z]+\.", b.name):
|
|
continue
|
|
reset_bone(b)
|
|
|
|
a_cam = bpy.data.objects.get(a.asset_data["camera"])
|
|
if a_cam:
|
|
context.scene.camera = a_cam
|
|
bpy.context.view_layer.update()
|
|
|
|
a.local_id.asset_generate_preview()
|
|
|
|
context.object.animation_data.action = _action
|
|
context.scene.camera = _camera
|
|
bpy.context.view_layer.update()
|
|
|
|
return {"FINISHED"}
|
|
|
|
|
|
class ACTIONLIB_OT_update_action_data(Operator):
|
|
bl_idname = "actionlib.update_action_data"
|
|
bl_options = {"REGISTER", "INTERNAL"}
|
|
bl_label = "Udpate Action Data"
|
|
bl_description = "Update Action Metadata"
|
|
|
|
tags: EnumProperty(
|
|
name="Tags",
|
|
items=(
|
|
("POSE", "pose", ""),
|
|
("ANIM", "anim", ""),
|
|
),
|
|
)
|
|
|
|
use_tags: BoolProperty(default=False)
|
|
use_camera: BoolProperty(default=False)
|
|
use_rest_pose: BoolProperty(default=False)
|
|
|
|
@classmethod
|
|
def poll(cls, context: Context) -> bool:
|
|
if context.id:
|
|
return True
|
|
|
|
def invoke(self, context, event):
|
|
scn = context.scene
|
|
scn.actionlib.camera = scn.camera
|
|
|
|
return context.window_manager.invoke_props_dialog(self)
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
layout.use_property_split = True
|
|
|
|
scn = context.scene
|
|
|
|
heading = layout.column(align=True, heading="Tags")
|
|
row = heading.row(align=True)
|
|
row.prop(self, "use_tags", text="")
|
|
sub = row.row()
|
|
sub.active = self.use_tags
|
|
sub.prop(self, "tags", text="")
|
|
|
|
heading = layout.column(align=True, heading="Camera")
|
|
row = heading.row(align=True)
|
|
row.prop(self, "use_camera", text="")
|
|
sub = row.row()
|
|
sub.active = self.use_camera
|
|
sub.prop(scn.actionlib, "camera", text="", icon="CAMERA_DATA")
|
|
|
|
heading = layout.column(align=True, heading="Rest Pose")
|
|
row = heading.row(align=True)
|
|
row.prop(self, "use_rest_pose", text="")
|
|
sub = row.row()
|
|
sub.active = self.use_rest_pose
|
|
sub.prop(scn.actionlib, "rest_pose", text="")
|
|
|
|
def execute(self, context):
|
|
scn = context.scene
|
|
|
|
for action in context.selected_files:
|
|
if self.use_camera:
|
|
action.asset_data["camera"] = context.scene.actionlib.camera.name
|
|
|
|
if self.use_tags:
|
|
tag = self.tags.lower()
|
|
not_tag = "anim" if tag == "pose" else "pose"
|
|
|
|
if tag not in action.asset_data.tags.keys():
|
|
if not_tag in action.asset_data.tags.keys():
|
|
for t in action.asset_data.tags:
|
|
if t != not_tag:
|
|
continue
|
|
action.asset_data.tags.remove(t)
|
|
|
|
action.asset_data.tags.new(tag)
|
|
|
|
if "pose" in action.asset_data.tags.keys():
|
|
action.asset_data["is_single_frame"] = True
|
|
else:
|
|
action.asset_data["is_single_frame"] = False
|
|
|
|
if self.use_rest_pose:
|
|
name = scn.actionlib.rest_pose.name if scn.actionlib.rest_pose else ""
|
|
action.asset_data["rest_pose"] = name
|
|
|
|
return {"FINISHED"}
|
|
|
|
|
|
class ACTIONLIB_OT_assign_rest_pose(Operator):
|
|
bl_idname = "actionlib.mark_as_rest_pose"
|
|
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
|
bl_label = "Mark As Rest Pose"
|
|
bl_description = "Mark Pose as Rest Pose"
|
|
|
|
@classmethod
|
|
def poll(cls, context: Context) -> bool:
|
|
if context.area.ui_type == "ASSETS":
|
|
return True
|
|
|
|
def execute(self, context: Context) -> Set[str]:
|
|
active_action = context.active_file
|
|
context.scene.actionlib.rest_pose = context.id
|
|
|
|
print(f"'{active_action.name.title()}' marked as Rest Pose.")
|
|
return {"FINISHED"}
|
|
|
|
|
|
class ACTIONLIB_OT_open_blendfile(Operator):
|
|
bl_idname = "actionlib.open_blendfile"
|
|
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
|
bl_label = "Set Paths"
|
|
bl_description = "Open Containing File"
|
|
|
|
replace_local: BoolProperty(default=False)
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
if not asset_utils.SpaceAssetInfo.is_asset_browser(context.space_data):
|
|
return False
|
|
|
|
sp = context.space_data
|
|
if sp.params.asset_library_ref == "LOCAL":
|
|
return False
|
|
|
|
return True
|
|
|
|
def execute(self, context: Context) -> Set[str]:
|
|
asset_path = get_asset_source(replace_local=self.replace_local)
|
|
|
|
cmd = get_bl_cmd(
|
|
blender=bpy.app.binary_path,
|
|
blendfile=str(asset_path),
|
|
)
|
|
subprocess.Popen(cmd)
|
|
return {"FINISHED"}
|
|
|
|
|
|
# LIBRARY_ITEMS = []
|
|
"""
|
|
def callback_operator(modal_func, operator, override={}):
|
|
|
|
def wrap(self, context, event):
|
|
ret, = retset = modal_func(self, context, event)
|
|
if ret in {'FINISHED'}:
|
|
|
|
with context.temp_override(**override):
|
|
callback()
|
|
|
|
return retset
|
|
return wrap
|
|
"""
|
|
|
|
|
|
class ACTIONLIB_OT_make_custom_preview(Operator):
|
|
bl_idname = "actionlib.make_custom_preview"
|
|
bl_label = "Custom Preview"
|
|
bl_description = "Set a camera to preview an asset"
|
|
|
|
def modal(self, context, event):
|
|
prefs = get_addons_prefs()
|
|
|
|
if not prefs.preview_modal:
|
|
with context.temp_override(
|
|
area=self.source_area, region=self.source_area.regions[-1]
|
|
):
|
|
bpy.ops.actionlib.store_anim_pose(
|
|
"INVOKE_DEFAULT", clear_previews=False, **prefs.add_asset_dict
|
|
)
|
|
return {"FINISHED"}
|
|
|
|
return {"PASS_THROUGH"}
|
|
|
|
def invoke(self, context, event):
|
|
|
|
self.source_area = bpy.context.area
|
|
|
|
view3d = get_viewport()
|
|
with context.temp_override(
|
|
area=view3d, region=view3d.regions[-1], window=context.window
|
|
):
|
|
# To close the popup
|
|
bpy.ops.screen.screen_full_area()
|
|
bpy.ops.screen.back_to_previous()
|
|
|
|
view3d = get_viewport()
|
|
with context.temp_override(
|
|
area=view3d, region=view3d.regions[-1], window=context.window
|
|
):
|
|
bpy.ops.assetlib.make_custom_preview("INVOKE_DEFAULT", modal=True)
|
|
|
|
context.window_manager.modal_handler_add(self)
|
|
return {"RUNNING_MODAL"}
|
|
|
|
|
|
def get_preview_items(self, context):
|
|
prefs = get_addon_prefs()
|
|
return sorted(
|
|
[
|
|
(k, k, "", v.icon_id, index)
|
|
for index, (k, v) in enumerate(prefs.previews.items())
|
|
],
|
|
reverse=True,
|
|
)
|
|
|
|
|
|
class ACTIONLIB_OT_store_anim_pose(Operator):
|
|
bl_idname = "actionlib.store_anim_pose"
|
|
bl_label = "Add Action to the current library"
|
|
bl_description = "Store current pose/anim to local library"
|
|
|
|
warning: StringProperty(name="")
|
|
path: StringProperty(name="Path")
|
|
catalog: StringProperty(
|
|
name="Catalog", update=asset_warning_callback, options={"TEXTEDIT_UPDATE"}
|
|
)
|
|
name: StringProperty(
|
|
name="Name", update=asset_warning_callback, options={"TEXTEDIT_UPDATE"}
|
|
)
|
|
action_type: EnumProperty(
|
|
name="Type", items=[("POSE", "Pose", ""), ("ANIMATION", "Animation", "")]
|
|
)
|
|
frame_start: IntProperty(name="Frame Start")
|
|
frame_end: IntProperty(name="Frame End")
|
|
tags: StringProperty(
|
|
name="Tags", description="Tags need to separate with a comma (,)"
|
|
)
|
|
description: StringProperty(name="Description")
|
|
preview: EnumProperty(items=get_preview_items)
|
|
clear_previews: BoolProperty(default=True)
|
|
store_library: StringProperty(name="Store Library")
|
|
|
|
@classmethod
|
|
def poll(cls, context: Context) -> bool:
|
|
ob = context.object
|
|
if not ob:
|
|
cls.poll_message_set(f"You have no active object")
|
|
return False
|
|
|
|
if not ob.type == "ARMATURE":
|
|
cls.poll_message_set(f"Active object {ob.name} is not of type ARMATURE")
|
|
return False
|
|
|
|
if not ob.animation_data or not ob.animation_data.action:
|
|
cls.poll_message_set(f"Active object {ob.name} has no action or keyframes")
|
|
return False
|
|
|
|
return True
|
|
|
|
# def draw_tags(self, asset, layout):
|
|
# row = layout.row()
|
|
# row.template_list("ASSETBROWSER_UL_metadata_tags", "asset_tags", asset.asset_data, "tags",
|
|
# asset.asset_data, "active_tag", rows=4)
|
|
|
|
# col = row.column(align=True)
|
|
# col.operator("asset.tag_add", icon='ADD', text="")
|
|
# col.operator("asset.tag_remove", icon='REMOVE', text="")
|
|
|
|
def to_dict(self):
|
|
keys = (
|
|
"catalog",
|
|
"name",
|
|
"action_type",
|
|
"frame_start",
|
|
"frame_end",
|
|
"tags",
|
|
"description",
|
|
"store_library",
|
|
)
|
|
return {k: getattr(self, k) for k in keys}
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
layout.separator()
|
|
prefs = get_addon_prefs()
|
|
|
|
split = layout.split(factor=0.39, align=True)
|
|
# row = split.row(align=False)
|
|
# split.use_property_split = False
|
|
split.alignment = "RIGHT"
|
|
|
|
split.label(text="Preview")
|
|
|
|
sub = split.row(align=True)
|
|
sub.template_icon_view(self, "preview", show_labels=False)
|
|
sub.separator()
|
|
sub.operator(
|
|
"actionlib.make_custom_preview", icon="RESTRICT_RENDER_OFF", text=""
|
|
)
|
|
|
|
prefs.add_asset_dict.clear()
|
|
prefs.add_asset_dict.update(self.to_dict())
|
|
|
|
sub.label(icon="BLANK1")
|
|
# layout.ui_units_x = 50
|
|
|
|
# row = layout.row(align=True)
|
|
layout.use_property_split = True
|
|
|
|
if self.current_library.merge_libraries:
|
|
layout.prop(self.current_library, "store_library", expand=False)
|
|
|
|
# row.label(text='Action Name')
|
|
layout.prop(self, "action_type", text="Type", expand=True)
|
|
if self.action_type == "ANIMATION":
|
|
col = layout.column(align=True)
|
|
col.prop(self, "frame_start")
|
|
col.prop(self, "frame_end", text="End")
|
|
|
|
layout.prop(self, "catalog", text="Catalog")
|
|
layout.prop(self, "name", text="Name")
|
|
|
|
# layout.separator()
|
|
# self.draw_tags(self.asset_action, layout)
|
|
layout.prop(self, "tags")
|
|
layout.prop(self, "description", text="Description")
|
|
# layout.prop(prefs, 'author', text='Author')
|
|
|
|
# layout.prop()
|
|
|
|
layout.separator()
|
|
col = layout.column()
|
|
col.use_property_split = False
|
|
# row.enabled = False
|
|
|
|
if self.path:
|
|
col.label(text=self.path)
|
|
|
|
if self.warning:
|
|
col.label(icon="ERROR", text=self.warning)
|
|
|
|
def set_action_type(self):
|
|
ob = bpy.context.object
|
|
action = ob.animation_data.action
|
|
|
|
bones = [b.name for b in bpy.context.selected_pose_bones_from_active_object]
|
|
|
|
keyframes_selected = get_keyframes(action, selected=True, includes=bones)
|
|
self.frame_start = min(keyframes_selected)
|
|
self.frame_end = max(keyframes_selected)
|
|
|
|
self.action_type = "POSE"
|
|
if self.frame_start != self.frame_end:
|
|
self.action_type = "ANIMATION"
|
|
|
|
def invoke(self, context, event):
|
|
prefs = get_addon_prefs()
|
|
ob = context.object
|
|
|
|
self.asset_action = ob.animation_data.action.copy()
|
|
self.asset_action.asset_mark()
|
|
self.area = context.area
|
|
self.current_library = get_active_library()
|
|
|
|
if self.store_library:
|
|
self.current_library.store_library = self.store_library
|
|
else:
|
|
lib = self.current_library.library_type.get_active_asset_library()
|
|
if lib.name:
|
|
self.current_library.store_library = lib.name
|
|
self.store_library = lib.name
|
|
|
|
# lib = self.current_library
|
|
self.tags = ""
|
|
|
|
# print(self, self.library_items)
|
|
catalog_item = lib.catalog.active_item
|
|
|
|
if catalog_item:
|
|
self.catalog = catalog_item.path # get_active_catalog()
|
|
|
|
self.set_action_type()
|
|
|
|
if self.clear_previews:
|
|
prefs.previews.clear()
|
|
|
|
view3d = get_viewport()
|
|
with context.temp_override(area=view3d, region=view3d.regions[-1]):
|
|
bpy.ops.assetlib.make_custom_preview("INVOKE_DEFAULT")
|
|
|
|
else:
|
|
preview_items = get_preview_items(self, context)
|
|
if preview_items:
|
|
self.preview = preview_items[0][0]
|
|
|
|
return context.window_manager.invoke_props_dialog(self, width=350)
|
|
|
|
def action_to_asset(self, action):
|
|
# action.asset_mark()
|
|
prefs = get_addon_prefs()
|
|
action.name = self.name
|
|
action.asset_generate_preview()
|
|
|
|
# Remove Keyframes
|
|
bones = [b.name for b in bpy.context.selected_pose_bones_from_active_object]
|
|
clean_action(
|
|
action=action,
|
|
frame_start=self.frame_start,
|
|
frame_end=self.frame_end,
|
|
excludes=["world", "walk"],
|
|
includes=bones,
|
|
)
|
|
|
|
## Define Tags
|
|
tags = [t.strip() for t in self.tags.split(",") if t]
|
|
tag_range = f"f{self.frame_start}"
|
|
is_single_frame = True
|
|
if self.action_type == "ANIM":
|
|
is_single_frame = False
|
|
tag_range = f"f{self.frame_start}-f{self.frame_end}"
|
|
|
|
tags.append(self.action_type)
|
|
tags.append(tag_range)
|
|
|
|
for tag in tags:
|
|
action.asset_data.tags.new(tag)
|
|
|
|
action.asset_data["is_single_frame"] = is_single_frame
|
|
action.asset_data["rig"] = bpy.context.object.name
|
|
|
|
action.asset_data.description = self.description
|
|
action.asset_data.author = prefs.author
|
|
|
|
col = get_overriden_col(bpy.context.object)
|
|
if col:
|
|
action.asset_data["col"] = col.name
|
|
|
|
return action
|
|
|
|
def render_animation(self, video_path):
|
|
ctx = bpy.context
|
|
scn = ctx.scene
|
|
vl = ctx.view_layer
|
|
area = get_view3d_persp()
|
|
space = area.spaces.active
|
|
|
|
attrs = [
|
|
(scn, "use_preview_range", True),
|
|
(scn, "frame_preview_start", self.frame_start),
|
|
(scn, "frame_preview_end", self.frame_end),
|
|
(scn.render, "resolution_percentage", 100),
|
|
(space.overlay, "show_overlays", False),
|
|
(space.region_3d, "view_perspective", "CAMERA"),
|
|
(scn.render, "resolution_x", 1280),
|
|
(scn.render, "resolution_y", 720),
|
|
(scn.render.image_settings, "file_format", "FFMPEG"),
|
|
(scn.render.ffmpeg, "format", "QUICKTIME"),
|
|
(scn.render.ffmpeg, "codec", "H264"),
|
|
(scn.render.ffmpeg, "ffmpeg_preset", "GOOD"),
|
|
(scn.render.ffmpeg, "constant_rate_factor", "HIGH"),
|
|
(scn.render.ffmpeg, "gopsize", 12),
|
|
(scn.render, "filepath", str(video_path)),
|
|
]
|
|
|
|
if self.action_type == "ANIMATION":
|
|
with attr_set(preview_attrs + video_attrs):
|
|
with ctx.temp_override(area=area):
|
|
bpy.ops.render.opengl(animation=True)
|
|
|
|
def save_pose_preview(self):
|
|
pass
|
|
|
|
def refresh(self, area):
|
|
bpy.ops.asset.library_refresh({"area": area, "region": area.regions[3]})
|
|
# space_data.activate_asset_by_id(asset, deferred=deferred)
|
|
|
|
def execute(self, context: Context):
|
|
|
|
scn = context.scene
|
|
vl = context.view_layer
|
|
ob = context.object
|
|
|
|
prefs = get_addon_prefs()
|
|
|
|
lib = self.current_library
|
|
if lib.merge_libraries:
|
|
lib = prefs.libraries[self.current_library.store_library]
|
|
|
|
lib_type = lib.library_type
|
|
|
|
asset_path = lib_type.get_asset_path(name=self.name, catalog=self.catalog)
|
|
img_path = lib_type.get_image_path(
|
|
name=self.name, catalog=self.catalog, filepath=asset_path
|
|
)
|
|
video_path = lib_type.get_video_path(
|
|
name=self.name, catalog=self.catalog, filepath=asset_path
|
|
)
|
|
|
|
## Copy Action
|
|
current_action = ob.animation_data.action
|
|
asset_action = self.asset_action
|
|
ob.animation_data.action = asset_action
|
|
vl.update()
|
|
|
|
self.action_to_asset(asset_action)
|
|
|
|
# lib_type.new_asset()
|
|
|
|
# Saving the video
|
|
if self.action_type == "ANIMATION":
|
|
self.render_animation(video_path)
|
|
|
|
# Saving the preview image
|
|
preview = prefs.previews[self.preview]
|
|
lib_type.write_preview(preview, img_path)
|
|
|
|
# Transfert the pixel to the action preview
|
|
pixels = [0] * preview.image_size[0] * preview.image_size[1] * 4
|
|
preview.image_pixels_float.foreach_get(pixels)
|
|
asset_action.preview_ensure().image_pixels_float.foreach_set(pixels)
|
|
|
|
lib_type.write_asset(asset=asset_action, asset_path=asset_path)
|
|
|
|
asset_data = dict(lib_type.get_asset_data(asset_action), catalog=self.catalog)
|
|
asset_info = lib_type.format_asset_info([asset_data], asset_path=asset_path)
|
|
|
|
# print('asset_info')
|
|
# pprint(asset_info)
|
|
|
|
diff = [dict(a, operation="ADD") for a in lib_type.flatten_cache([asset_info])]
|
|
|
|
ob.animation_data.action = current_action
|
|
|
|
asset_action.asset_clear()
|
|
asset_action.use_fake_user = False
|
|
bpy.data.actions.remove(asset_action)
|
|
|
|
# TODO Write a proper method for this
|
|
diff_path = Path(bpy.app.tempdir, "diff.json")
|
|
|
|
# diff = [dict(a, operation='ADD') for a in [asset_info])]
|
|
diff_path.write_text(json.dumps(diff, indent=4))
|
|
|
|
bpy.ops.assetlib.bundle(name=lib.name, diff=str(diff_path), blocking=True)
|
|
|
|
# self.area.tag_redraw()
|
|
|
|
self.report({"INFO"}, f'"{self.name}" has been added to the library.')
|
|
|
|
return {"FINISHED"}
|
|
|
|
|
|
classes = (
|
|
ACTIONLIB_OT_assign_action,
|
|
ACTIONLIB_OT_restore_previous_action,
|
|
# ACTIONLIB_OT_publish,
|
|
ACTIONLIB_OT_apply_anim,
|
|
ACTIONLIB_OT_replace_pose,
|
|
ACTIONLIB_OT_create_anim_asset,
|
|
ACTIONLIB_OT_apply_selected_action,
|
|
ACTIONLIB_OT_edit_action,
|
|
ACTIONLIB_OT_clear_action,
|
|
ACTIONLIB_OT_generate_preview,
|
|
ACTIONLIB_OT_update_action_data,
|
|
ACTIONLIB_OT_assign_rest_pose,
|
|
ACTIONLIB_OT_store_anim_pose,
|
|
ACTIONLIB_OT_make_custom_preview,
|
|
)
|
|
|
|
register, unregister = bpy.utils.register_classes_factory(classes)
|