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 .symmetrize_shape_key import mirror_mesh
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.
DEFORM_MODIFIERS = [
@ -99,6 +100,12 @@ class PoseShapeKey(PropertyGroup):
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(
name="Action",
type=Action,
@ -359,26 +366,7 @@ class OBJECT_OT_pose_key_set_pose(Operator):
@classmethod
def poll(cls, context):
obj = context.object
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
return poll_has_psk_and_deformed(cls, context)
def execute(self, 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]
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.
"""
def poll_has_psk_and_deformed(operator, context):
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
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.
if not (
@ -435,6 +439,47 @@ def poll_correct_pose_key_pose(operator, context):
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):
"""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
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):
return False
@ -729,7 +789,9 @@ class OBJECT_OT_pose_key_clamp_influence(Operator):
@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.")
cls.poll_message_set(
"No shape keys of this pose shape key use vertex masks. There is nothing to clamp."
)
return False
return True
@ -776,7 +838,9 @@ class OBJECT_OT_pose_key_place_objects_in_grid(Operator):
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.")
cls.poll_message_set(
"This pose key has no storage objects, so there is nothing to sort into a grid."
)
return False
return True
@ -894,6 +958,7 @@ class OBJECT_OT_pose_key_copy_data(Operator):
registry = [
PoseShapeKeyTarget,
PoseShapeKey,
OBJECT_OT_pose_key_add,
OBJECT_OT_pose_key_save,
OBJECT_OT_pose_key_set_pose,
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'
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)
@ -103,6 +108,7 @@ class MESH_PT_pose_keys(Panel):
list_context_path='object.data.pose_keys',
active_idx_context_path='object.data.active_pose_key_index',
menu_class_name='MESH_MT_pose_key_utils',
add_op_name='object.posekey_add',
)
layout.use_property_split = True
@ -126,7 +132,10 @@ class MESH_PT_pose_keys(Panel):
else:
layout.operator('object.posekey_set_pose', text="Set Pose", icon="ARMATURE_DATA")
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="")
return

View File

@ -118,6 +118,10 @@ def draw_ui_list(
list_context_path='object.data.vertex_groups',
active_idx_context_path='object.data.vertex_groups.active_index',
insertion_operators=True,
add_op_name=None,
add_kwargs={},
remove_op_name=None,
remove_kwargs={},
move_operators=True,
menu_class_name='',
**kwargs,
@ -148,13 +152,19 @@ def draw_ui_list(
col = row.column()
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.active_idx_context_path = active_idx_context_path
row = col.row()
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.active_idx_context_path = active_idx_context_path