Pose Shape Keys: Major Update #321

Merged
Demeter Dzadik merged 10 commits from pose-shape-keys-extension into main 2024-07-03 14:50:58 +02:00
3 changed files with 114 additions and 30 deletions
Showing only changes of commit f044a3270e - Show all commits

View File

@ -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,

View File

@ -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

View File

@ -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