Pose Shape Keys: Major Update #321
@ -5,6 +5,7 @@ from mathutils import Vector
|
|||||||
from math import sqrt
|
from math import sqrt
|
||||||
from .symmetrize_shape_key import mirror_mesh
|
from .symmetrize_shape_key import mirror_mesh
|
||||||
from .prefs import get_addon_prefs
|
from .prefs import get_addon_prefs
|
||||||
|
from .ui_list import UILIST_OT_Entry_Add
|
||||||
|
|
||||||
# When saving or pushing shapes, disable any modifier NOT in this list.
|
# When saving or pushing shapes, disable any modifier NOT in this list.
|
||||||
DEFORM_MODIFIERS = [
|
DEFORM_MODIFIERS = [
|
||||||
@ -99,6 +100,12 @@ class PoseShapeKey(PropertyGroup):
|
|||||||
|
|
||||||
active_target_shape_index: IntProperty(update=update_active_sk_index)
|
active_target_shape_index: IntProperty(update=update_active_sk_index)
|
||||||
|
|
||||||
|
def update_name(self, context):
|
||||||
|
if self.name == "":
|
||||||
|
self.name = "Pose Key"
|
||||||
|
|
||||||
|
name: StringProperty(name="Name", update=update_name)
|
||||||
|
|
||||||
action: PointerProperty(
|
action: PointerProperty(
|
||||||
name="Action",
|
name="Action",
|
||||||
type=Action,
|
type=Action,
|
||||||
@ -359,26 +366,7 @@ class OBJECT_OT_pose_key_set_pose(Operator):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context):
|
def poll(cls, context):
|
||||||
obj = context.object
|
return poll_has_psk_and_deformed(cls, context)
|
||||||
if not obj or obj.type != 'MESH':
|
|
||||||
cls.poll_message_set("Active object must be a mesh.")
|
|
||||||
return False
|
|
||||||
arm_ob = get_deforming_armature(obj)
|
|
||||||
if not arm_ob:
|
|
||||||
cls.poll_message_set("Mesh must be deformed by an Armature modifier.")
|
|
||||||
return False
|
|
||||||
if not obj.data.shape_keys:
|
|
||||||
cls.poll_message_set("Mesh must have shape keys.")
|
|
||||||
return False
|
|
||||||
if len(obj.data.pose_keys) == 0:
|
|
||||||
cls.poll_message_set("Mesh must have pose keys.")
|
|
||||||
return True
|
|
||||||
pose_key = obj.data.pose_keys[obj.data.active_pose_key_index]
|
|
||||||
if not pose_key.action:
|
|
||||||
cls.poll_message_set("Pose Key must have an Action assigned.")
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
set_pose_of_active_pose_key(context)
|
set_pose_of_active_pose_key(context)
|
||||||
@ -394,25 +382,41 @@ def get_active_pose_key(obj):
|
|||||||
return obj.data.pose_keys[obj.data.active_pose_key_index]
|
return obj.data.pose_keys[obj.data.active_pose_key_index]
|
||||||
|
|
||||||
|
|
||||||
def poll_correct_pose_key_pose(operator, context):
|
def poll_has_psk_and_deformed(operator, context):
|
||||||
"""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
|
obj = context.object
|
||||||
|
|
||||||
if not obj:
|
if not obj:
|
||||||
operator.poll_message_set("There must be an active mesh object.")
|
operator.poll_message_set("There must be an active mesh object.")
|
||||||
|
return False
|
||||||
|
|
||||||
pose_key = get_active_pose_key(obj)
|
pose_key = get_active_pose_key(obj)
|
||||||
if not pose_key:
|
if not pose_key:
|
||||||
operator.poll_message_set("A Pose Shape Key must be selected.")
|
operator.poll_message_set("A Pose Shape Key must be selected.")
|
||||||
return False
|
return False
|
||||||
|
if not pose_key.name:
|
||||||
|
operator.poll_message_set("The Pose Shape Key must be named.")
|
||||||
|
|
||||||
arm_ob = get_deforming_armature(obj)
|
arm_ob = get_deforming_armature(obj)
|
||||||
|
|
||||||
if not arm_ob:
|
if not arm_ob:
|
||||||
operator.poll_message_set("This mesh object is not deformed by any Armature modifier.")
|
operator.poll_message_set("This mesh object is not deformed by any Armature modifier.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
return obj, pose_key, arm_ob
|
||||||
|
|
||||||
|
|
||||||
|
def poll_correct_pose_key_pose(operator, context):
|
||||||
|
"""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_psk_arm = poll_has_psk_and_deformed(operator, context)
|
||||||
|
|
||||||
|
if not obj_psk_arm:
|
||||||
|
return False
|
||||||
|
|
||||||
|
obj, pose_key, arm_ob = obj_psk_arm
|
||||||
|
|
||||||
# Action must exist and match.
|
# Action must exist and match.
|
||||||
if not (
|
if not (
|
||||||
@ -435,6 +439,47 @@ def poll_correct_pose_key_pose(operator, context):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
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'}
|
||||||
|
|
||||||
|
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):
|
||||||
|
self.layout.prop(self, 'pose_key_name')
|
||||||
|
if not self.pose_key_name:
|
||||||
|
self.layout.alert = True
|
||||||
|
self.layout.label(text="Name cannot be empty.", icon='ERROR')
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
if not self.pose_key_name:
|
||||||
|
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_save(Operator, OperatorWithWarning, SaveAndRestoreState):
|
class OBJECT_OT_pose_key_save(Operator, OperatorWithWarning, SaveAndRestoreState):
|
||||||
"""Save the current evaluated mesh vertex positions into the Storage Object"""
|
"""Save the current evaluated mesh vertex positions into the Storage Object"""
|
||||||
|
|
||||||
@ -444,6 +489,21 @@ class OBJECT_OT_pose_key_save(Operator, OperatorWithWarning, SaveAndRestoreState
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context):
|
def poll(cls, context):
|
||||||
|
obj_psk_arm = poll_has_psk_and_deformed(cls, context)
|
||||||
|
if not obj_psk_arm:
|
||||||
|
return False
|
||||||
|
obj, pose_key, arm_ob = obj_psk_arm
|
||||||
|
if (
|
||||||
|
not pose_key.storage_object
|
||||||
|
and not pose_key.action
|
||||||
|
and arm_ob.animation_data
|
||||||
|
and arm_ob.animation_data.action
|
||||||
|
):
|
||||||
|
# If we can guess the action, allow the operator to run.
|
||||||
|
# Let's call this "Initialize mode", since we're allowing the
|
||||||
|
# user to 1-click initialize some variables.
|
||||||
|
return True
|
||||||
|
|
||||||
if not poll_correct_pose_key_pose(cls, context):
|
if not poll_correct_pose_key_pose(cls, context):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@ -729,7 +789,9 @@ class OBJECT_OT_pose_key_clamp_influence(Operator):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context):
|
def poll(cls, context):
|
||||||
if not cls.get_affected_vertex_group_names(context.object):
|
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.")
|
cls.poll_message_set(
|
||||||
|
"No shape keys of this pose shape key use vertex masks. There is nothing to clamp."
|
||||||
|
)
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -776,7 +838,9 @@ class OBJECT_OT_pose_key_place_objects_in_grid(Operator):
|
|||||||
def poll(cls, context):
|
def poll(cls, context):
|
||||||
"""Only available if there are any storage objects in any of the pose keys."""
|
"""Only available if there are any storage objects in any of the pose keys."""
|
||||||
if not cls.get_storage_objects(context):
|
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.")
|
cls.poll_message_set(
|
||||||
|
"This pose key has no storage objects, so there is nothing to sort into a grid."
|
||||||
|
)
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -894,6 +958,7 @@ class OBJECT_OT_pose_key_copy_data(Operator):
|
|||||||
registry = [
|
registry = [
|
||||||
PoseShapeKeyTarget,
|
PoseShapeKeyTarget,
|
||||||
PoseShapeKey,
|
PoseShapeKey,
|
||||||
|
OBJECT_OT_pose_key_add,
|
||||||
OBJECT_OT_pose_key_save,
|
OBJECT_OT_pose_key_save,
|
||||||
OBJECT_OT_pose_key_set_pose,
|
OBJECT_OT_pose_key_set_pose,
|
||||||
OBJECT_OT_pose_key_push,
|
OBJECT_OT_pose_key_push,
|
||||||
|
@ -19,6 +19,11 @@ class CK_UL_pose_keys(UIList):
|
|||||||
|
|
||||||
icon = 'SURFACE_NCIRCLE' if pose_key.storage_object else 'CURVE_NCIRCLE'
|
icon = 'SURFACE_NCIRCLE' if pose_key.storage_object else 'CURVE_NCIRCLE'
|
||||||
name_row = split.row()
|
name_row = split.row()
|
||||||
|
if not pose_key.name:
|
||||||
|
name_row.alert = True
|
||||||
|
split = name_row.split()
|
||||||
|
name_row = split.row()
|
||||||
|
split.label(text="Unnamed!", icon='ERROR')
|
||||||
name_row.prop(pose_key, 'name', text="", emboss=False, icon=icon)
|
name_row.prop(pose_key, 'name', text="", emboss=False, icon=icon)
|
||||||
|
|
||||||
|
|
||||||
@ -103,6 +108,7 @@ class MESH_PT_pose_keys(Panel):
|
|||||||
list_context_path='object.data.pose_keys',
|
list_context_path='object.data.pose_keys',
|
||||||
active_idx_context_path='object.data.active_pose_key_index',
|
active_idx_context_path='object.data.active_pose_key_index',
|
||||||
menu_class_name='MESH_MT_pose_key_utils',
|
menu_class_name='MESH_MT_pose_key_utils',
|
||||||
|
add_op_name='object.posekey_add',
|
||||||
)
|
)
|
||||||
|
|
||||||
layout.use_property_split = True
|
layout.use_property_split = True
|
||||||
@ -126,7 +132,10 @@ class MESH_PT_pose_keys(Panel):
|
|||||||
else:
|
else:
|
||||||
layout.operator('object.posekey_set_pose', text="Set Pose", icon="ARMATURE_DATA")
|
layout.operator('object.posekey_set_pose', text="Set Pose", icon="ARMATURE_DATA")
|
||||||
row = layout.row()
|
row = layout.row()
|
||||||
row.operator('object.posekey_save', text="Store Evaluated Mesh", icon="FILE_TICK")
|
text = "Store Posed Mesh"
|
||||||
|
if not active_posekey.storage_object and not active_posekey.action:
|
||||||
|
text = "Store Mesh & Init Pose Key"
|
||||||
|
row.operator('object.posekey_save', text=text, icon="FILE_TICK")
|
||||||
row.prop(active_posekey, 'storage_object', text="")
|
row.prop(active_posekey, 'storage_object', text="")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -118,6 +118,10 @@ def draw_ui_list(
|
|||||||
list_context_path='object.data.vertex_groups',
|
list_context_path='object.data.vertex_groups',
|
||||||
active_idx_context_path='object.data.vertex_groups.active_index',
|
active_idx_context_path='object.data.vertex_groups.active_index',
|
||||||
insertion_operators=True,
|
insertion_operators=True,
|
||||||
|
add_op_name=None,
|
||||||
|
add_kwargs={},
|
||||||
|
remove_op_name=None,
|
||||||
|
remove_kwargs={},
|
||||||
move_operators=True,
|
move_operators=True,
|
||||||
menu_class_name='',
|
menu_class_name='',
|
||||||
**kwargs,
|
**kwargs,
|
||||||
@ -148,13 +152,19 @@ def draw_ui_list(
|
|||||||
|
|
||||||
col = row.column()
|
col = row.column()
|
||||||
if insertion_operators:
|
if insertion_operators:
|
||||||
add_op = col.operator('ui.list_entry_add', text="", icon='ADD')
|
op_name = add_op_name or 'ui.list_entry_add'
|
||||||
|
add_op = col.operator(op_name, text="", icon='ADD')
|
||||||
|
for key, value in add_kwargs.items():
|
||||||
|
setattr(add_op, key, value)
|
||||||
add_op.list_context_path = list_context_path
|
add_op.list_context_path = list_context_path
|
||||||
add_op.active_idx_context_path = active_idx_context_path
|
add_op.active_idx_context_path = active_idx_context_path
|
||||||
|
|
||||||
row = col.row()
|
row = col.row()
|
||||||
row.enabled = len(my_list) > 0
|
row.enabled = len(my_list) > 0
|
||||||
remove_op = row.operator('ui.list_entry_remove', text="", icon='REMOVE')
|
op_name = remove_op_name or 'ui.list_entry_remove'
|
||||||
|
remove_op = row.operator(op_name, text="", icon='REMOVE')
|
||||||
|
for key, value in remove_kwargs.items():
|
||||||
|
setattr(remove_op, key, value)
|
||||||
remove_op.list_context_path = list_context_path
|
remove_op.list_context_path = list_context_path
|
||||||
remove_op.active_idx_context_path = active_idx_context_path
|
remove_op.active_idx_context_path = active_idx_context_path
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user