1261 lines
45 KiB
Python
1261 lines
45 KiB
Python
# SPDX-FileCopyrightText: 2025 Blender Studio Tools Authors
|
|
#
|
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
import re
|
|
from collections import OrderedDict
|
|
|
|
import bpy
|
|
from bpy.types import Object, Operator
|
|
from bpy.props import StringProperty, BoolProperty, EnumProperty
|
|
from mathutils import Vector, Quaternion
|
|
from math import sqrt
|
|
|
|
from .symmetrize_shape_key import mirror_mesh
|
|
from .prefs import get_addon_prefs
|
|
from .ui_list import UILIST_OT_Entry_Add, UILIST_OT_Entry_Remove
|
|
from .naming import side_is_left
|
|
|
|
# When saving or pushing shapes, disable any modifier NOT in this list.
|
|
DEFORM_MODIFIERS = [
|
|
'ARMATURE',
|
|
'CAST',
|
|
'CURVE',
|
|
'DISPLACE',
|
|
'HOOK',
|
|
'LAPLACIANDEFORM',
|
|
'LATTICE',
|
|
'MESH_DEFORM',
|
|
'SHRINKWRAP',
|
|
'SIMPLE_DEFORM',
|
|
'SMOOTH',
|
|
'CORRECTIVE_SMOOTH',
|
|
'LAPLACIANSMOOTH',
|
|
'SURFACE_DEFORM',
|
|
'WARP',
|
|
'WAVE',
|
|
]
|
|
GOOD_MODIFIERS = ['ARMATURE']
|
|
|
|
|
|
class OBJECT_OT_pose_key_add(UILIST_OT_Entry_Add, Operator):
|
|
"""Add Pose Shape Key"""
|
|
|
|
bl_idname = "object.posekey_add"
|
|
bl_label = "Add Pose Shape Key"
|
|
bl_options = {'UNDO', 'REGISTER', 'INTERNAL'}
|
|
bl_property = "pose_key_name" # Focus the text input box
|
|
|
|
list_context_path: StringProperty()
|
|
active_idx_context_path: StringProperty()
|
|
|
|
pose_key_name: StringProperty(name="Name", default="Pose Key")
|
|
|
|
def invoke(self, context, event):
|
|
return context.window_manager.invoke_props_dialog(self)
|
|
|
|
def draw(self, context):
|
|
layout = self.layout.column()
|
|
layout.use_property_split = True
|
|
|
|
layout.prop(self, 'pose_key_name')
|
|
if self.pose_key_name.strip() == "":
|
|
layout.alert = True
|
|
layout.label(text="Name cannot be empty.", icon='ERROR')
|
|
|
|
def execute(self, context):
|
|
if self.pose_key_name.strip() == "":
|
|
self.report({'ERROR'}, "Must specify a name.")
|
|
return {'CANCELLED'}
|
|
|
|
my_list = self.get_list(context)
|
|
active_index = self.get_active_index(context)
|
|
|
|
to_index = active_index + 1
|
|
if len(my_list) == 0:
|
|
to_index = 0
|
|
|
|
psk = my_list.add()
|
|
psk.name = self.pose_key_name
|
|
my_list.move(len(my_list) - 1, to_index)
|
|
self.set_active_index(context, to_index)
|
|
|
|
return {'FINISHED'}
|
|
|
|
|
|
class OBJECT_OT_pose_key_auto_init(Operator):
|
|
"""Assign the current Action and scene frame number to this pose key"""
|
|
|
|
bl_idname = "object.posekey_auto_init"
|
|
bl_label = "Initialize From Context"
|
|
bl_options = {'UNDO', 'REGISTER', 'INTERNAL'}
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
obj = context.object
|
|
arm_ob = get_deforming_armature(obj)
|
|
if not arm_ob:
|
|
cls.poll_message_set("No deforming armature.")
|
|
return False
|
|
if not (arm_ob.animation_data and arm_ob.animation_data.action):
|
|
cls.poll_message_set("Armature has no Action assigned.")
|
|
return False
|
|
obj = context.object
|
|
pose_key = get_active_pose_key(obj)
|
|
if (
|
|
pose_key.action == arm_ob.animation_data.action
|
|
and pose_key.frame == context.scene.frame_current
|
|
):
|
|
cls.poll_message_set("Action and frame number are already set.")
|
|
return False
|
|
return True
|
|
|
|
def execute(self, context):
|
|
# Set action and frame number to the current ones.
|
|
obj = context.object
|
|
pose_key = get_active_pose_key(obj)
|
|
arm_ob = get_deforming_armature(obj)
|
|
pose_key.action = arm_ob.animation_data.action
|
|
pose_key.frame = context.scene.frame_current
|
|
self.report({'INFO'}, "Initialized Pose Key data.")
|
|
return {'FINISHED'}
|
|
|
|
|
|
class OBJECT_OT_pose_key_set_pose(Operator):
|
|
"""Reset the rig, then set the above Action and frame number"""
|
|
|
|
bl_idname = "object.posekey_set_pose"
|
|
bl_label = "Set Pose"
|
|
bl_options = {'UNDO', 'REGISTER', 'INTERNAL'}
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
return poll_correct_pose_key_pose(cls, context, demand_pose=False)
|
|
|
|
def execute(self, context):
|
|
set_pose_of_active_pose_key(context)
|
|
return {'FINISHED'}
|
|
|
|
|
|
class SaveAndRestoreState:
|
|
def disable_non_deform_modifiers(self, storage_ob: Object, rigged_ob: Object):
|
|
# Disable non-deforming modifiers
|
|
self.disabled_mods_storage = []
|
|
self.disabled_mods_rigged = []
|
|
self.disabled_fcurves = []
|
|
for obj, lst in zip(
|
|
[storage_ob, rigged_ob], [self.disabled_mods_storage, self.disabled_mods_rigged]
|
|
):
|
|
if not obj:
|
|
continue
|
|
for mod in obj.modifiers:
|
|
if mod.type not in GOOD_MODIFIERS and mod.show_viewport:
|
|
lst.append(mod.name)
|
|
mod.show_viewport = False
|
|
if mod.show_viewport:
|
|
data_path = f'modifiers["{mod.name}"].show_viewport'
|
|
fc = obj.animation_data.drivers.find(data_path)
|
|
if fc:
|
|
fc.mute = True
|
|
self.disabled_fcurves.append(data_path)
|
|
mod.show_viewport = False
|
|
|
|
def restore_non_deform_modifiers(self, storage_ob: Object, rigged_ob: Object):
|
|
# Re-enable non-deforming modifiers
|
|
for obj, mod_list in zip(
|
|
[storage_ob, rigged_ob], [self.disabled_mods_storage, self.disabled_mods_rigged]
|
|
):
|
|
if not obj:
|
|
continue
|
|
for mod_namee in mod_list:
|
|
obj.modifiers[mod_namee].show_viewport = True
|
|
for data_path in self.disabled_fcurves:
|
|
fc = obj.animation_data.drivers.find(data_path)
|
|
if fc:
|
|
fc.mute = False
|
|
|
|
def save_state(self, context):
|
|
rigged_ob = context.object
|
|
|
|
pose_key = get_active_pose_key(rigged_ob)
|
|
storage_ob = pose_key.storage_object
|
|
|
|
# Non-Deforming modifiers
|
|
self.disable_non_deform_modifiers(storage_ob, rigged_ob)
|
|
|
|
# Active Shape Key Index
|
|
self.orig_sk_index = rigged_ob.active_shape_key_index
|
|
rigged_ob.active_shape_key_index = 0
|
|
|
|
# Shape Keys
|
|
self.org_sk_toggles = {}
|
|
for target_shape in pose_key.target_shapes:
|
|
key_block = target_shape.key_block
|
|
if not key_block:
|
|
self.report({'ERROR'}, f"Shape key not found: {key_block.name}")
|
|
return {'CANCELLED'}
|
|
self.org_sk_toggles[key_block.name] = key_block.mute
|
|
key_block.mute = True
|
|
|
|
def restore_state(self, context):
|
|
rigged_ob = context.object
|
|
pose_key = get_active_pose_key(rigged_ob)
|
|
storage_ob = pose_key.storage_object
|
|
self.restore_non_deform_modifiers(storage_ob, rigged_ob)
|
|
|
|
rigged_ob.active_shape_key_index = self.orig_sk_index
|
|
for kb_name, kb_value in self.org_sk_toggles.items():
|
|
rigged_ob.data.shape_keys.key_blocks[kb_name].mute = kb_value
|
|
|
|
|
|
class OperatorWithWarning:
|
|
def invoke(self, context, event):
|
|
addon_prefs = get_addon_prefs(context)
|
|
if addon_prefs.no_warning:
|
|
return self.execute(context)
|
|
|
|
return context.window_manager.invoke_props_dialog(self, width=400)
|
|
|
|
def draw(self, context):
|
|
layout = self.layout.column(align=True)
|
|
|
|
warning = self.get_warning_text(context)
|
|
for line in warning.split("\n"):
|
|
row = layout.row()
|
|
row.alert = True
|
|
row.label(text=line)
|
|
|
|
addon_prefs = get_addon_prefs(context)
|
|
col = layout.column(align=True)
|
|
col.prop(addon_prefs, 'no_warning', text="Disable Warnings (Can be reset in Preferences)")
|
|
|
|
def get_warning_text(self, context):
|
|
raise NotImplemented
|
|
|
|
|
|
class OBJECT_OT_pose_key_save(Operator, OperatorWithWarning, SaveAndRestoreState):
|
|
"""Save the deformed mesh vertex positions of the current pose into the Storage Object"""
|
|
|
|
bl_idname = "object.posekey_save"
|
|
bl_label = "Overwrite Storage Object"
|
|
bl_options = {'UNDO', 'REGISTER', 'INTERNAL'}
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
return poll_correct_pose_key_pose(cls, context)
|
|
|
|
def invoke(self, context, event):
|
|
obj = context.object
|
|
pose_key = get_active_pose_key(obj)
|
|
if pose_key.storage_object:
|
|
return super().invoke(context, event)
|
|
return self.execute(context)
|
|
|
|
def get_warning_text(self, context):
|
|
obj = context.object
|
|
pose_key = get_active_pose_key(obj)
|
|
return f'Overwrite storage object "{pose_key.storage_object.name}"?'
|
|
|
|
def execute(self, context):
|
|
rigged_ob = context.object
|
|
|
|
pose_key = get_active_pose_key(rigged_ob)
|
|
storage_ob = pose_key.storage_object
|
|
already_existed = storage_ob != None
|
|
self.disable_non_deform_modifiers(storage_ob, rigged_ob)
|
|
|
|
depsgraph = context.evaluated_depsgraph_get()
|
|
rigged_ob_eval = rigged_ob.evaluated_get(depsgraph)
|
|
rigged_ob_eval_mesh = rigged_ob_eval.data
|
|
|
|
storage_ob_name = rigged_ob.name + "-" + pose_key.name
|
|
storage_ob_mesh = bpy.data.meshes.new_from_object(rigged_ob)
|
|
storage_ob_mesh.name = storage_ob_name
|
|
|
|
if not already_existed:
|
|
storage_ob = bpy.data.objects.new(storage_ob_name, storage_ob_mesh)
|
|
context.scene.collection.objects.link(storage_ob)
|
|
pose_key.storage_object = storage_ob
|
|
storage_ob.location = rigged_ob.location
|
|
storage_ob.location.x -= rigged_ob.dimensions.x * 1.1
|
|
else:
|
|
old_mesh = storage_ob.data
|
|
storage_ob.data = storage_ob_mesh
|
|
bpy.data.meshes.remove(old_mesh)
|
|
|
|
if len(storage_ob.data.vertices) != len(rigged_ob.data.vertices):
|
|
self.report(
|
|
{'WARNING'},
|
|
f'Vertex Count did not match between storage object {storage_ob.name}({len(storage_ob.data.vertices)}) and current ({len(rigged_ob.data.vertices)})!',
|
|
)
|
|
storage_ob_mesh = bpy.data.meshes.new_from_object(rigged_ob_eval)
|
|
storage_ob.data = storage_ob_mesh
|
|
storage_ob.data.name = storage_ob_name
|
|
|
|
storage_ob.use_shape_key_edit_mode = True
|
|
storage_ob.shape_key_add(name="Basis")
|
|
target = storage_ob.shape_key_add(name="Morph Target")
|
|
adjust = storage_ob.shape_key_add(name="New Changes", from_mix=True)
|
|
target.value = 1
|
|
adjust.value = 1
|
|
storage_ob.active_shape_key_index = 2
|
|
|
|
# Fix material assignments in case any material slots are linked to the
|
|
# object instead of the mesh.
|
|
for i, ms in enumerate(rigged_ob.material_slots):
|
|
if ms.link == 'OBJECT':
|
|
storage_ob.material_slots[i].link = 'OBJECT'
|
|
storage_ob.material_slots[i].material = ms.material
|
|
|
|
# Set the target shape to be the evaluated mesh.
|
|
for target_v, eval_v in zip(target.data, rigged_ob_eval_mesh.vertices):
|
|
target_v.co = eval_v.co
|
|
|
|
# Copy some symmetry settings from the original
|
|
storage_ob.data.use_mirror_x = rigged_ob.data.use_mirror_x
|
|
|
|
# Nuke vertex groups, since we don't need them.
|
|
storage_ob.vertex_groups.clear()
|
|
|
|
self.restore_non_deform_modifiers(storage_ob, rigged_ob)
|
|
|
|
# If new shape is visible and it already existed, set it as active.
|
|
if already_existed and storage_ob.visible_get():
|
|
bpy.ops.object.mode_set(mode='OBJECT')
|
|
bpy.ops.object.select_all(action='DESELECT')
|
|
context.view_layer.objects.active = storage_ob
|
|
storage_ob.select_set(True)
|
|
|
|
self.report({'INFO'}, f'The deformed mesh has been stored in "{storage_ob.name}".')
|
|
return {'FINISHED'}
|
|
|
|
|
|
class OBJECT_OT_pose_key_push(Operator, OperatorWithWarning, SaveAndRestoreState):
|
|
"""Let the below shape keys blend the current deformed shape into the shape of the Storage Object"""
|
|
|
|
bl_idname = "object.posekey_push"
|
|
bl_label = "Load Vertex Position Data into Shape Keys"
|
|
bl_options = {'UNDO', 'REGISTER', 'INTERNAL'}
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
if not poll_correct_pose_key_pose(cls, context):
|
|
return False
|
|
|
|
# No shape keys to push into.
|
|
obj = context.object
|
|
pose_key = get_active_pose_key(obj)
|
|
for target_shape in pose_key.target_shapes:
|
|
if target_shape.key_block:
|
|
return True
|
|
|
|
cls.poll_message_set(
|
|
"This Pose Key doesn't have any target shape keys to push into. Add some in the Shape Key Slots list below."
|
|
)
|
|
return False
|
|
|
|
def get_warning_text(self, context):
|
|
obj = context.object
|
|
pose_key = get_active_pose_key(obj)
|
|
target_shape_names = [target.name for target in pose_key.target_shapes if target]
|
|
return (
|
|
"This will overwrite the following Shape Keys: \n "
|
|
+ "\n ".join(target_shape_names)
|
|
+ "\n Are you sure?"
|
|
)
|
|
|
|
def execute(self, context):
|
|
"""
|
|
Load the active PoseShapeKey's mesh data into its corresponding shape key,
|
|
such that the shape key will blend from whatever state the mesh is currently in,
|
|
into the shape stored in the PoseShapeKey.
|
|
"""
|
|
|
|
self.save_state(context)
|
|
|
|
try:
|
|
self.push_active_pose_key(context, set_pose=False)
|
|
except:
|
|
return {'CANCELLED'}
|
|
|
|
self.restore_state(context)
|
|
|
|
return {'FINISHED'}
|
|
|
|
def push_active_pose_key(self, context, set_pose=False):
|
|
depsgraph = context.evaluated_depsgraph_get()
|
|
scene = context.scene
|
|
|
|
rigged_ob = context.object
|
|
|
|
pose_key = get_active_pose_key(rigged_ob)
|
|
|
|
storage_object = pose_key.storage_object
|
|
if storage_object.name not in context.view_layer.objects:
|
|
self.report({'ERROR'}, f'Storage object "{storage_object.name}" must be in view layer!')
|
|
raise Exception
|
|
|
|
if set_pose:
|
|
set_pose_of_active_pose_key(context)
|
|
|
|
# The Pose Key stores the vertex positions of a previous evaluated mesh.
|
|
# This, and the current vertex positions of the mesh are subtracted
|
|
# from each other to get the difference in their shape.
|
|
storage_eval_verts = pose_key.storage_object.evaluated_get(depsgraph).data.vertices
|
|
rigged_eval_verts = rigged_ob.evaluated_get(depsgraph).data.vertices
|
|
|
|
# Shape keys are relative to the base shape of the mesh, so that delta
|
|
# will be added to the base mesh to get the final shape key vertex positions.
|
|
rigged_base_verts = rigged_ob.data.vertices
|
|
|
|
# The CrazySpace provides us the matrix by which each vertex has been
|
|
# deformed by modifiers and shape keys. This matrix is necessary to
|
|
# calculate the correct delta.
|
|
rigged_ob.crazyspace_eval(depsgraph, scene)
|
|
|
|
for i, v in enumerate(storage_eval_verts):
|
|
if i > len(rigged_base_verts) - 1:
|
|
break
|
|
storage_eval_co = Vector(v.co)
|
|
rigged_eval_co = rigged_eval_verts[i].co
|
|
|
|
delta = storage_eval_co - rigged_eval_co
|
|
|
|
delta = rigged_ob.crazyspace_displacement_to_original(
|
|
vertex_index=i, displacement=delta
|
|
)
|
|
|
|
base_v = rigged_base_verts[i].co
|
|
for target_shape in pose_key.target_shapes:
|
|
key_block = target_shape.key_block
|
|
if not key_block:
|
|
continue
|
|
key_block.data[i].co = base_v + delta
|
|
|
|
# Mirror shapes if needed
|
|
for target_shape in pose_key.target_shapes:
|
|
if target_shape.mirror_x:
|
|
key_block = target_shape.key_block
|
|
if not key_block:
|
|
continue
|
|
mirror_mesh(
|
|
reference_verts=rigged_ob.data.vertices,
|
|
vertices=key_block.data,
|
|
axis='X',
|
|
symmetrize=False,
|
|
)
|
|
|
|
rigged_ob.crazyspace_eval_clear()
|
|
|
|
if len(storage_eval_verts) != len(rigged_eval_verts):
|
|
self.report(
|
|
{'WARNING'},
|
|
f'Mismatching topology: Stored shape "{pose_key.storage_object.name}" had {len(storage_eval_verts)} vertices instead of {len(rigged_eval_verts)}',
|
|
)
|
|
|
|
|
|
class OBJECT_OT_pose_key_push_all(Operator, OperatorWithWarning, SaveAndRestoreState):
|
|
"""Go through all Pose Keys, set their pose and overwrite the shape keys to match the storage object shapes"""
|
|
|
|
bl_idname = "object.posekey_push_all"
|
|
bl_label = "Push ALL Pose Keys into Shape Keys"
|
|
bl_options = {'UNDO', 'REGISTER'}
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
obj = context.object
|
|
if not obj or obj.type != 'MESH':
|
|
cls.poll_message_set("No active mesh object.")
|
|
return False
|
|
if len(obj.data.pose_keys) == 0:
|
|
cls.poll_message_set("No Pose Shape Keys to push.")
|
|
return False
|
|
return True
|
|
|
|
def get_warning_text(self, context):
|
|
obj = context.object
|
|
target_shape_names = []
|
|
for pk in obj.data.pose_keys:
|
|
target_shape_names.extend([t.name for t in pk.target_shapes if t])
|
|
return (
|
|
"This will overwrite the following Shape Keys: \n "
|
|
+ "\n ".join(target_shape_names)
|
|
+ "\n Are you sure?"
|
|
)
|
|
|
|
def execute(self, context):
|
|
rigged_ob = context.object
|
|
for i, pk in enumerate(rigged_ob.data.pose_keys):
|
|
rigged_ob.data.active_pose_key_index = i
|
|
bpy.ops.object.posekey_set_pose()
|
|
bpy.ops.object.posekey_push()
|
|
|
|
return {'FINISHED'}
|
|
|
|
|
|
class OBJECT_OT_pose_key_clamp_influence(Operator):
|
|
"""Clamp the influence of this pose key's shape keys to 1.0 for each vertex, by normalizing the vertex weight mask values of vertices where the total influence is greater than 1"""
|
|
|
|
bl_idname = "object.posekey_clamp_influence"
|
|
bl_label = "Clamp Vertex Influences"
|
|
bl_options = {'UNDO', 'REGISTER', 'INTERNAL'}
|
|
|
|
@staticmethod
|
|
def get_affected_vertex_group_names(object: Object) -> list[str]:
|
|
pose_key = get_active_pose_key(object)
|
|
|
|
vg_names = []
|
|
for target_shape in pose_key.target_shapes:
|
|
kb = target_shape.key_block
|
|
if not kb:
|
|
continue
|
|
if kb.vertex_group and kb.vertex_group in object.vertex_groups:
|
|
vg_names.append(kb.vertex_group)
|
|
|
|
return vg_names
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
if not cls.get_affected_vertex_group_names(context.object):
|
|
cls.poll_message_set(
|
|
"No shape keys of this pose shape key use vertex masks. There is nothing to clamp."
|
|
)
|
|
return False
|
|
return True
|
|
|
|
def normalize_vgroups(self, obj, vgroups):
|
|
"""Normalize a set of vertex groups in isolation"""
|
|
""" Used for creating mask vertex groups for splitting shape keys """
|
|
for vert in obj.data.vertices:
|
|
# Find sum of weights in specified vgroups
|
|
# set weight to original/sum
|
|
sum_weights = 0
|
|
for vgroup in vgroups:
|
|
try:
|
|
sum_weights += vgroup.weight(vert.index)
|
|
except:
|
|
pass
|
|
for vgroup in vgroups:
|
|
if sum_weights > 1.0:
|
|
try:
|
|
vgroup.add([vert.index], vgroup.weight(vert.index) / sum_weights, 'REPLACE')
|
|
except:
|
|
pass
|
|
|
|
def execute(self, context):
|
|
obj = context.object
|
|
vg_names = self.get_affected_vertex_group_names(obj)
|
|
self.normalize_vgroups(obj, [obj.vertex_groups[vg_name] for vg_name in vg_names])
|
|
return {'FINISHED'}
|
|
|
|
|
|
class OBJECT_OT_pose_key_place_objects_in_grid(Operator):
|
|
"""Place the storage objects in a grid above this object"""
|
|
|
|
bl_idname = "object.posekey_object_grid"
|
|
bl_label = "Place ALL Storage Objects in a Grid"
|
|
bl_options = {'UNDO', 'REGISTER', 'INTERNAL'}
|
|
|
|
@staticmethod
|
|
def get_storage_objects(context) -> list[Object]:
|
|
obj = context.object
|
|
pose_keys = obj.data.pose_keys
|
|
return [pk.storage_object for pk in pose_keys if pk.storage_object]
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
"""Only available if there are any storage objects in any of the pose keys."""
|
|
if not cls.get_storage_objects(context):
|
|
cls.poll_message_set(
|
|
"This pose key has no storage objects, so there is nothing to sort into a grid."
|
|
)
|
|
return False
|
|
return True
|
|
|
|
@staticmethod
|
|
def place_objects_in_grid(context, objs: list[Object]):
|
|
if not objs:
|
|
return
|
|
x = max([obj.dimensions.x for obj in objs])
|
|
y = max([obj.dimensions.y for obj in objs])
|
|
z = max([obj.dimensions.z for obj in objs])
|
|
scalar = 1.2
|
|
dimensions = Vector((x * scalar, y * scalar, z * scalar))
|
|
|
|
grid_rows = round(sqrt(len(objs)))
|
|
for i, obj in enumerate(objs):
|
|
col_i = (i % grid_rows) - int(grid_rows / 2)
|
|
row_i = int(i / grid_rows) + scalar
|
|
offset = Vector((col_i * dimensions.x, 0, row_i * dimensions.z))
|
|
obj.location = context.object.location + offset
|
|
|
|
def execute(self, context):
|
|
storage_objects = self.get_storage_objects(context)
|
|
self.place_objects_in_grid(context, storage_objects)
|
|
|
|
return {'FINISHED'}
|
|
|
|
|
|
class OBJECT_OT_pose_key_jump_to_storage(Operator):
|
|
"""Place the storage object next to this object and select it"""
|
|
|
|
bl_idname = "object.posekey_jump_to_storage"
|
|
bl_label = "Jump To Storage Object"
|
|
bl_options = {'UNDO', 'REGISTER', 'INTERNAL'}
|
|
|
|
@staticmethod
|
|
def get_storage_object(context):
|
|
obj = context.object
|
|
pose_key = get_active_pose_key(obj)
|
|
return pose_key.storage_object
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
"""Only available if there is a storage object in the pose key."""
|
|
if not cls.get_storage_object(context):
|
|
cls.poll_message_set("This pose key doesn't have a storage object to jump to.")
|
|
return False
|
|
return True
|
|
|
|
def execute(self, context):
|
|
storage_object = self.get_storage_object(context)
|
|
|
|
storage_object.location = context.object.location
|
|
storage_object.location.x -= context.object.dimensions.x * 1.1
|
|
|
|
if storage_object.name not in context.view_layer.objects:
|
|
self.report({'ERROR'}, "Storage object must be in view layer.")
|
|
return {'CANCELLED'}
|
|
bpy.ops.object.select_all(action='DESELECT')
|
|
storage_object.select_set(True)
|
|
storage_object.hide_set(False)
|
|
context.view_layer.objects.active = storage_object
|
|
|
|
# Put the other storage objects in a grid
|
|
prefs = get_addon_prefs(context)
|
|
if prefs.grid_objects_on_jump:
|
|
storage_objects = OBJECT_OT_pose_key_place_objects_in_grid.get_storage_objects(context)
|
|
storage_objects.remove(storage_object)
|
|
OBJECT_OT_pose_key_place_objects_in_grid.place_objects_in_grid(context, storage_objects)
|
|
|
|
return {'FINISHED'}
|
|
|
|
|
|
class OBJECT_OT_pose_key_copy_data(Operator):
|
|
"""Copy Pose Key data from active object to selected ones"""
|
|
|
|
bl_idname = "object.posekey_copy_data"
|
|
bl_label = "Copy Pose Key Data"
|
|
bl_options = {'UNDO', 'REGISTER', 'INTERNAL'}
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
"""Only available if there is a selected mesh and the active mesh has pose key data."""
|
|
selected_meshes = [obj for obj in context.selected_objects if obj.type == 'MESH']
|
|
if len(selected_meshes) < 2:
|
|
cls.poll_message_set("No other meshes are selected to copy pose key data to.")
|
|
return False
|
|
if context.object.type != 'MESH' or not context.object.data.pose_keys:
|
|
cls.poll_message_set("No active mesh object with pose keys to copy.")
|
|
return False
|
|
return True
|
|
|
|
def execute(self, context):
|
|
source_ob = context.object
|
|
targets = [
|
|
obj for obj in context.selected_objects if obj.type == 'MESH' and obj != source_ob
|
|
]
|
|
|
|
for target_ob in targets:
|
|
target_ob.data.pose_keys.clear()
|
|
|
|
for src_pk in source_ob.data.pose_keys:
|
|
new_pk = target_ob.data.pose_keys.add()
|
|
new_pk.name = src_pk.name
|
|
new_pk.action = src_pk.action
|
|
new_pk.frame = src_pk.frame
|
|
new_pk.storage_object = src_pk.storage_object
|
|
for src_sk_slot in src_pk.target_shapes:
|
|
new_sk_slot = new_pk.target_shapes.add()
|
|
new_sk_slot.name = src_sk_slot.name
|
|
new_sk_slot.mirror_x = src_sk_slot.mirror_x
|
|
|
|
return {'FINISHED'}
|
|
|
|
|
|
class OBJECT_OT_pose_key_shape_add(UILIST_OT_Entry_Add, Operator):
|
|
"""Add Target Shape Key"""
|
|
|
|
bl_idname = "object.posekey_shape_add"
|
|
bl_label = "Add Target Shape Key"
|
|
bl_options = {'UNDO', 'REGISTER', 'INTERNAL'}
|
|
|
|
list_context_path: StringProperty()
|
|
active_idx_context_path: StringProperty()
|
|
|
|
def update_sk_name(self, context):
|
|
def set_vg(vg_name):
|
|
obj = context.object
|
|
vg = obj.vertex_groups.get(vg_name)
|
|
if vg:
|
|
self.vg_name = vg.name
|
|
return vg
|
|
|
|
vg = set_vg(self.sk_name)
|
|
if not vg and self.sk_name.endswith(".L"):
|
|
vg = set_vg("Side.L")
|
|
if not vg and self.sk_name.endswith(".R"):
|
|
vg = set_vg("Side.R")
|
|
|
|
sk_name: StringProperty(
|
|
name="Name",
|
|
description="Name to set for the new shape key",
|
|
default="Key",
|
|
update=update_sk_name,
|
|
)
|
|
def update_create_sk(self, context):
|
|
obj = context.object
|
|
if not self.create_sk and not obj.data.shape_keys:
|
|
# If there are no shape keys, force enable creation of new shape key.
|
|
self.create_sk = True
|
|
return
|
|
pose_key = get_active_pose_key(obj)
|
|
if self.create_sk:
|
|
self.sk_name = pose_key.name
|
|
elif self.sk_name not in context.object.data.shape_keys.key_blocks:
|
|
self.sk_name = ""
|
|
|
|
create_sk: BoolProperty(
|
|
name="Create New Shape Key",
|
|
description="Create a new blank Shape Key to push this pose into, instead of using an existing one",
|
|
default=True,
|
|
update=update_create_sk
|
|
)
|
|
vg_name: StringProperty(
|
|
name="Vertex Group",
|
|
description="Vertex Group to assign as the masking group of this shape key",
|
|
default="",
|
|
)
|
|
|
|
def update_create_vg(self, context):
|
|
if self.create_vg:
|
|
self.vg_name = self.sk_name
|
|
elif self.vg_name not in context.object.vertex_groups:
|
|
self.vg_name = ""
|
|
|
|
create_vg: BoolProperty(
|
|
name="Create New Vertex Group",
|
|
description="Create a new blank Vertex Group as a mask for this shape key, as opposed to using an existing vertex group for masking",
|
|
default=False,
|
|
update=update_create_vg,
|
|
)
|
|
|
|
create_slot: BoolProperty(
|
|
name="Create New Slot",
|
|
description="Internal. Whether to assign the chosen (or created) shape key to the current slot, or to create a new one",
|
|
default=True,
|
|
)
|
|
|
|
def invoke(self, context, event):
|
|
obj = context.object
|
|
if obj.data.shape_keys:
|
|
self.sk_name = f"Key {len(obj.data.shape_keys.key_blocks)}"
|
|
else:
|
|
self.sk_name = "Key"
|
|
self.create_sk = True
|
|
|
|
pose_key = get_active_pose_key(obj)
|
|
if pose_key.name:
|
|
self.sk_name = pose_key.name
|
|
if not self.create_slot and pose_key.active_target:
|
|
self.sk_name = pose_key.active_target.name
|
|
|
|
return context.window_manager.invoke_props_dialog(self, width=350)
|
|
|
|
def draw(self, context):
|
|
layout = self.layout.column()
|
|
layout.use_property_split = True
|
|
|
|
obj = context.object
|
|
|
|
row = layout.row(align=True)
|
|
if self.create_sk:
|
|
row.prop(self, 'sk_name', icon='SHAPEKEY_DATA')
|
|
else:
|
|
row.prop_search(
|
|
self, 'sk_name', obj.data.shape_keys, 'key_blocks', icon='SHAPEKEY_DATA'
|
|
)
|
|
|
|
if obj.data.shape_keys:
|
|
# We don't want to draw this option if there are no shape keys. (Since it would have to be True.)
|
|
row.prop(self, 'create_sk', text="", icon='ADD')
|
|
|
|
if (self.create_sk and self.sk_name) or obj.data.shape_keys.key_blocks.get(self.sk_name):
|
|
row = layout.row(align=True)
|
|
if self.create_vg:
|
|
if obj.vertex_groups.get(self.vg_name):
|
|
row.alert = True
|
|
layout.label(
|
|
text="Cannot create that vertex group because it already exists!", icon='ERROR'
|
|
)
|
|
row.prop(self, 'vg_name', icon='GROUP_VERTEX')
|
|
else:
|
|
row.prop_search(self, 'vg_name', obj, "vertex_groups")
|
|
|
|
row.prop(self, 'create_vg', text="", icon='ADD')
|
|
|
|
def execute(self, context):
|
|
if self.sk_name.strip() == "":
|
|
self.report({'ERROR'}, "Must provide shape key name.")
|
|
return {'CANCELLED'}
|
|
obj = context.object
|
|
|
|
if self.create_vg and obj.vertex_groups.get(self.vg_name):
|
|
self.report({'ERROR'}, f"Vertex group '{self.vg_name}' already exists!")
|
|
return {'CANCELLED'}
|
|
|
|
# Ensure Basis shape key
|
|
if not obj.data.shape_keys:
|
|
basis = obj.shape_key_add()
|
|
basis.name = "Basis"
|
|
obj.data.update()
|
|
|
|
if self.create_sk:
|
|
# Add new shape key.
|
|
key_block = obj.shape_key_add()
|
|
key_block.name = self.sk_name
|
|
key_block.value = 1
|
|
else:
|
|
key_block = obj.data.shape_keys.key_blocks.get(self.sk_name)
|
|
|
|
if key_block:
|
|
if self.create_vg:
|
|
obj.vertex_groups.new(name=self.vg_name)
|
|
|
|
if self.vg_name:
|
|
key_block.vertex_group = self.vg_name
|
|
|
|
pose_key = get_active_pose_key(obj)
|
|
|
|
if self.create_slot:
|
|
super().execute(context)
|
|
|
|
if key_block:
|
|
target_slot = pose_key.active_target
|
|
target_slot.name = key_block.name
|
|
self.report({'INFO'}, f"Added shape key {key_block.name}.")
|
|
else:
|
|
self.report({'ERROR'}, "Failed to add shape key.")
|
|
|
|
obj.data.active_pose_key_index = obj.data.active_pose_key_index
|
|
|
|
return {'FINISHED'}
|
|
|
|
|
|
class OBJECT_OT_pose_key_shape_remove(UILIST_OT_Entry_Remove, OperatorWithWarning, Operator):
|
|
"""Remove Target Shape Key. Hold Shift to only remove the slot and keep the actual shape key"""
|
|
|
|
bl_idname = "object.posekey_shape_remove"
|
|
bl_label = "Remove Target Shape Key"
|
|
bl_options = {'UNDO', 'REGISTER', 'INTERNAL'}
|
|
|
|
list_context_path: StringProperty()
|
|
active_idx_context_path: StringProperty()
|
|
|
|
delete_sk: BoolProperty(
|
|
name="Delete Shape Key",
|
|
description="Delete the underlying Shape Key",
|
|
default=True,
|
|
)
|
|
|
|
def get_key_block(self, context):
|
|
obj = context.object
|
|
pose_key = get_active_pose_key(obj)
|
|
target_slot = pose_key.active_target
|
|
return target_slot.key_block
|
|
|
|
def invoke(self, context, event):
|
|
if self.get_key_block(context):
|
|
# If this actually targets a shape key, prompt for removal.
|
|
self.delete_sk = not event.shift
|
|
if self.delete_sk:
|
|
return super().invoke(context, event)
|
|
return self.execute(context)
|
|
|
|
def get_warning_text(self, context):
|
|
return "Delete this Shape Key?"
|
|
|
|
@staticmethod
|
|
def delete_shapekey_with_drivers(obj, key_block):
|
|
shape_key = key_block.id_data
|
|
if shape_key.animation_data:
|
|
for fcurve in shape_key.animation_data.drivers:
|
|
if fcurve.data_path.startswith(f'key_blocks["{key_block.name}"]'):
|
|
shape_key.animation_data.drivers.remove(fcurve)
|
|
obj.shape_key_remove(key_block)
|
|
|
|
def execute(self, context):
|
|
obj = context.object
|
|
|
|
key_block = self.get_key_block(context)
|
|
if key_block and self.delete_sk:
|
|
self.delete_shapekey_with_drivers(obj, key_block)
|
|
|
|
super().execute(context)
|
|
|
|
self.report({'INFO'}, f"Removed shape key slot.")
|
|
return {'FINISHED'}
|
|
|
|
|
|
class OBJECT_OT_pose_key_magic_driver(Operator):
|
|
"""Automatically drive this shape key based on current pose"""
|
|
|
|
bl_idname = "object.posekey_magic_driver"
|
|
bl_label = "Auto-initialize Driver"
|
|
bl_options = {'UNDO', 'REGISTER', 'INTERNAL'}
|
|
|
|
shapekey_name: StringProperty()
|
|
|
|
side: EnumProperty(
|
|
name="Side",
|
|
items=[
|
|
('LEFT', "Left", "Exclude bones whose names indicate right-side."),
|
|
('BOTH', "Both", "Consider the transformations of all bones, regardless of which side they're on."),
|
|
('RIGHT', "Right", "Exclude bones whose names indicate left-side."),
|
|
],
|
|
default='BOTH',
|
|
)
|
|
|
|
only_selected: BoolProperty(
|
|
name="Only Selected Bones",
|
|
description="Only consider selected bones for the driver set-up",
|
|
default=False
|
|
)
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
return poll_correct_pose_key_pose(cls, context)
|
|
|
|
def get_driving_channels(self, context) -> OrderedDict[str, tuple[str, float]]:
|
|
obj = context.object
|
|
arm_ob = get_deforming_armature(obj)
|
|
|
|
|
|
channels = OrderedDict()
|
|
|
|
EPSILON = 0.0001
|
|
|
|
for pb in arm_ob.pose.bones:
|
|
bone_is_left = side_is_left(pb.name)
|
|
if self.side != 'BOTH':
|
|
if (bone_is_left and self.side=='RIGHT') or (bone_is_left==False and self.side=='LEFT'):
|
|
continue
|
|
if self.only_selected and not pb.bone.select:
|
|
continue
|
|
bone_channels = OrderedDict({'loc' : [], 'rot': [], 'scale': []})
|
|
|
|
for axis in "xyz":
|
|
value = getattr(pb.location, axis)
|
|
if abs(value) > EPSILON:
|
|
bone_channels['loc'].append((axis.upper(), value))
|
|
channels[pb.name] = bone_channels
|
|
|
|
if len(pb.rotation_mode) == 3:
|
|
# Euler rotation: Check each axis.
|
|
value = getattr(pb.rotation_euler, axis)
|
|
if abs(value) > EPSILON:
|
|
bone_channels['rot'].append((axis.upper(), value))
|
|
channels[pb.name] = bone_channels
|
|
else:
|
|
if pb.rotation_mode == 'QUATERNION':
|
|
euler_rot = pb.rotation_quaternion.to_euler()
|
|
elif pb.rotation_mode == 'AXIS_ANGLE':
|
|
quat = Quaternion(Vector(pb.rotation_axis_angle).yzw, pb.rotation_axis_angle[0])
|
|
euler_rot = quat.to_euler()
|
|
|
|
value = getattr(euler_rot, axis)
|
|
if abs(value) > EPSILON:
|
|
bone_channels['rot'].append((axis.upper(), value))
|
|
channels[pb.name] = bone_channels
|
|
|
|
value = getattr(pb.scale, axis)
|
|
if abs(value-1) > EPSILON:
|
|
bone_channels['scale'].append((axis.upper(), value))
|
|
channels[pb.name] = bone_channels
|
|
|
|
return channels
|
|
|
|
@staticmethod
|
|
def sanitize_variable_name(name):
|
|
# Replace all non-word characters with underscores
|
|
sanitized = re.sub(r'\W', '_', name)
|
|
# Prepend underscore if the first character is a digit
|
|
if sanitized and sanitized[0].isdigit():
|
|
sanitized = '_' + sanitized
|
|
return sanitized
|
|
|
|
def invoke(self, context, event):
|
|
key_is_left = side_is_left(self.shapekey_name)
|
|
if key_is_left:
|
|
self.side = 'LEFT'
|
|
elif key_is_left == False:
|
|
self.side = 'RIGHT'
|
|
else:
|
|
self.side = 'BOTH'
|
|
return context.window_manager.invoke_props_dialog(self, width=300)
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
layout.label(text="Driver will be created based on the current pose.")
|
|
|
|
layout.use_property_split = True
|
|
layout.prop(self, 'only_selected')
|
|
layout.prop(self, 'side', expand=True, text="Side")
|
|
|
|
driving_channels = self.get_driving_channels(context)
|
|
|
|
if not driving_channels:
|
|
layout.label(text="No driving channels found.")
|
|
return
|
|
|
|
obj = context.object
|
|
arm_ob = get_deforming_armature(obj)
|
|
|
|
col = layout.column(align=True)
|
|
for bone_name, transforms in driving_channels.items():
|
|
pb = arm_ob.pose.bones.get(bone_name)
|
|
bone_box = col.box()
|
|
bone_box.prop(pb, 'name', icon='BONE_DATA', text="", emboss=False)
|
|
for transform, trans_inf in transforms.items():
|
|
axes = [f"{inf[0]} ({val:.2f})" for inf, val in trans_inf]
|
|
if not axes:
|
|
continue
|
|
|
|
if transform == 'rot':
|
|
icon = 'CON_ROTLIKE'
|
|
elif transform == 'scale':
|
|
icon = 'CON_SIZELIKE'
|
|
else:
|
|
icon = 'CON_LOCLIKE'
|
|
bone_box.row().label(text=", ".join(axes), icon=icon)
|
|
|
|
def execute(self, context):
|
|
driving_channels = self.get_driving_channels(context)
|
|
if not driving_channels:
|
|
self.report({'ERROR'}, "No transform channels found to drive the shape key.")
|
|
return {'CANCELLED'}
|
|
count = 0
|
|
for bone_name, transforms in driving_channels.items():
|
|
for transform, trans_inf in transforms.items():
|
|
for axis, value in trans_inf:
|
|
count += 1
|
|
if count > 16:
|
|
self.report({'ERROR'}, f"Too many transform channels ({count}>16). Can't fit all in the driver expression.")
|
|
return {'CANCELLED'}
|
|
|
|
obj = context.object
|
|
arm_ob = get_deforming_armature(obj)
|
|
key_block = obj.data.shape_keys.key_blocks.get(self.shapekey_name)
|
|
|
|
key_block.driver_remove('value')
|
|
fc = key_block.driver_add('value')
|
|
|
|
if not self.setup_driver(fc, arm_ob, driving_channels):
|
|
return {'CANCELLED'}
|
|
|
|
self.report({'INFO'}, "Created shape key driver.")
|
|
return {'FINISHED'}
|
|
|
|
def setup_driver(self, fcurve, target, driving_channels, short=False) -> str:
|
|
expressions = []
|
|
|
|
for m in fcurve.modifiers:
|
|
# Drivers created via Python are initialized with a modifier that does nothing,
|
|
# except it prevents the insertion of keyframes. Weird as hell.
|
|
fcurve.modifiers.remove(m)
|
|
fcurve.keyframe_points.insert(0, 0)
|
|
fcurve.keyframe_points.insert(1, 1)
|
|
fcurve.extrapolation = 'LINEAR'
|
|
driver = fcurve.driver
|
|
|
|
for var in reversed(driver.variables):
|
|
driver.variables.remove(var)
|
|
|
|
for bone_name, transforms in driving_channels.items():
|
|
for transform, trans_inf in transforms.items():
|
|
for axis, value in trans_inf:
|
|
transf_type = transform.upper()+"_"+axis
|
|
var = driver.variables.new()
|
|
if short:
|
|
# Earlier code should early-exit before this gets to q.
|
|
var.name = "abcdefghijklmnopqrstuvwxyz"[len(driver.variables)-1]
|
|
else:
|
|
var.name = self.sanitize_variable_name(bone_name) + "_" + transf_type.lower()
|
|
var.type = 'TRANSFORMS'
|
|
var.targets[0].id = target
|
|
var.targets[0].bone_target = bone_name
|
|
var.targets[0].transform_type = transf_type
|
|
var.targets[0].rotation_mode = 'SWING_TWIST_Y'
|
|
var.targets[0].transform_space = 'LOCAL_SPACE'
|
|
if value < 0:
|
|
clamped_var = f"min(0, {var.name})"
|
|
else:
|
|
clamped_var = f"max(0, {var.name})"
|
|
if short:
|
|
value = f"{value:.2f}"
|
|
else:
|
|
value = f"{value:.4f}"
|
|
base = f"{clamped_var}/{value}"
|
|
if transf_type.startswith("SCALE"):
|
|
expressions.append(f"((1-{base})")
|
|
else:
|
|
expressions.append(f"({base})")
|
|
|
|
final_exp = " * ".join(expressions)
|
|
if short:
|
|
final_exp = final_exp.replace(" ", "")
|
|
if len(final_exp) > 255:
|
|
# I can't believe the expression length is this limited. Just to save 4 bytes in memory... Was it worth it?
|
|
if not short:
|
|
final_exp = self.setup_driver(driver, target, driving_channels, short=True)
|
|
else:
|
|
self.report({'ERROR'}, f"Too many driving channels ({len(driving_channels)}) to fit into expression. Blame Blender. Try to reduce number of channels.")
|
|
return final_exp
|
|
driver.expression = final_exp
|
|
return final_exp
|
|
|
|
def get_deforming_armature(mesh_ob: Object) -> Object | None:
|
|
for mod in mesh_ob.modifiers:
|
|
if mod.type == 'ARMATURE':
|
|
return mod.object
|
|
|
|
|
|
def reset_rig(rig, *, reset_transforms=True, reset_props=True, pbones=[]):
|
|
if not pbones:
|
|
pbones = rig.pose.bones
|
|
for pb in pbones:
|
|
if reset_transforms:
|
|
pb.location = (0, 0, 0)
|
|
pb.rotation_euler = (0, 0, 0)
|
|
pb.rotation_quaternion = (1, 0, 0, 0)
|
|
pb.scale = (1, 1, 1)
|
|
|
|
if not reset_props or len(pb.keys()) == 0:
|
|
continue
|
|
|
|
rna_properties = [prop.identifier for prop in pb.bl_rna.properties if prop.is_runtime]
|
|
|
|
# Reset custom property values to their default value
|
|
for key in pb.keys():
|
|
if key.startswith("$"):
|
|
continue
|
|
if key in rna_properties:
|
|
continue # Addon defined property.
|
|
|
|
property_settings = None
|
|
try:
|
|
property_settings = pb.id_properties_ui(key)
|
|
if not property_settings:
|
|
continue
|
|
property_settings = property_settings.as_dict()
|
|
if not 'default' in property_settings:
|
|
continue
|
|
except TypeError:
|
|
# Some properties don't support UI data, and so don't have a default value. (like addon PropertyGroups)
|
|
pass
|
|
|
|
if not property_settings:
|
|
continue
|
|
|
|
if type(pb[key]) not in (float, int, bool):
|
|
continue
|
|
pb[key] = property_settings['default']
|
|
|
|
|
|
def set_pose_of_active_pose_key(context):
|
|
rigged_ob = context.object
|
|
pose_key = get_active_pose_key(rigged_ob)
|
|
|
|
arm_ob = get_deforming_armature(rigged_ob)
|
|
reset_rig(arm_ob)
|
|
if pose_key.action:
|
|
# Set Action and Frame to get the right pose
|
|
arm_ob.animation_data.action = pose_key.action
|
|
context.scene.frame_current = pose_key.frame
|
|
|
|
|
|
def poll_correct_pose_key_pose(operator, context, demand_pose=True):
|
|
"""To make these operators foolproof, there are a lot of checks to make sure
|
|
that the user gets to see the effect of the operator. The "Set Pose" operator
|
|
can be used first to set the correct state and pass all the checks here.
|
|
"""
|
|
|
|
obj = context.object
|
|
|
|
if not obj:
|
|
operator.poll_message_set("There must be an active mesh object.")
|
|
return False
|
|
|
|
pose_key = get_active_pose_key(obj)
|
|
if not pose_key:
|
|
operator.poll_message_set("A Pose Shape Key must be selected.")
|
|
return False
|
|
if not pose_key.name:
|
|
operator.poll_message_set("The Pose Shape Key must be named.")
|
|
|
|
arm_ob = get_deforming_armature(obj)
|
|
|
|
if not arm_ob:
|
|
operator.poll_message_set("This mesh object is not deformed by any Armature modifier.")
|
|
return False
|
|
|
|
if not pose_key.action:
|
|
operator.poll_message_set("An Action must be associated with the Pose Shape Key.")
|
|
return False
|
|
|
|
if demand_pose:
|
|
# Action must exist and match.
|
|
if not (
|
|
arm_ob.animation_data
|
|
and arm_ob.animation_data.action
|
|
and arm_ob.animation_data.action == pose_key.action
|
|
):
|
|
operator.poll_message_set(
|
|
"The armature must have the Pose Shape Key's action assigned. Use the Set Pose button."
|
|
)
|
|
return False
|
|
|
|
if pose_key.frame != context.scene.frame_current:
|
|
operator.poll_message_set(
|
|
"The Pose Shape Key's frame must be the same as the current scene frame. Use the Set Pose button."
|
|
)
|
|
return False
|
|
|
|
return True
|
|
|
|
|
|
def get_active_pose_key(obj):
|
|
if obj.type != 'MESH':
|
|
return
|
|
if len(obj.data.pose_keys) == 0 or obj.data.active_pose_key_index >= len(obj.data.pose_keys):
|
|
return
|
|
|
|
return obj.data.pose_keys[obj.data.active_pose_key_index]
|
|
|
|
|
|
registry = [
|
|
OBJECT_OT_pose_key_auto_init,
|
|
OBJECT_OT_pose_key_add,
|
|
OBJECT_OT_pose_key_save,
|
|
OBJECT_OT_pose_key_set_pose,
|
|
OBJECT_OT_pose_key_push,
|
|
OBJECT_OT_pose_key_push_all,
|
|
OBJECT_OT_pose_key_clamp_influence,
|
|
OBJECT_OT_pose_key_place_objects_in_grid,
|
|
OBJECT_OT_pose_key_jump_to_storage,
|
|
OBJECT_OT_pose_key_copy_data,
|
|
OBJECT_OT_pose_key_shape_add,
|
|
OBJECT_OT_pose_key_shape_remove,
|
|
OBJECT_OT_pose_key_magic_driver,
|
|
]
|