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 215 additions and 88 deletions
Showing only changes of commit 485f405d83 - Show all commits

View File

@ -1,12 +1,12 @@
import bpy import bpy
from bpy.types import Object, Operator from bpy.types import Object, Operator
from bpy.props import StringProperty from bpy.props import StringProperty, BoolProperty
from mathutils import Vector 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 from .ui_list import UILIST_OT_Entry_Add, UILIST_OT_Entry_Remove
# 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 = [
@ -47,10 +47,13 @@ class OBJECT_OT_pose_key_add(UILIST_OT_Entry_Add, Operator):
return context.window_manager.invoke_props_dialog(self) return context.window_manager.invoke_props_dialog(self)
def draw(self, context): def draw(self, context):
self.layout.prop(self, 'pose_key_name') layout = self.layout.column()
layout.use_property_split=True
layout.prop(self, 'pose_key_name')
if not self.pose_key_name: if not self.pose_key_name:
self.layout.alert = True layout.alert = True
self.layout.label(text="Name cannot be empty.", icon='ERROR') layout.label(text="Name cannot be empty.", icon='ERROR')
def execute(self, context): def execute(self, context):
if not self.pose_key_name: if not self.pose_key_name:
@ -482,84 +485,6 @@ class OBJECT_OT_pose_key_push_all(Operator, OperatorWithWarning, SaveAndRestoreS
return {'FINISHED'} return {'FINISHED'}
class OBJECT_OT_create_shape_key_for_pose(Operator):
"""Create and assign a Shape Key"""
bl_idname = "object.create_shape_key_for_pose"
bl_label = "Create Shape Key"
bl_options = {'UNDO', 'REGISTER', 'INTERNAL'}
bl_property = "sk_name"
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
obj = context.object
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,
)
vg_name: StringProperty(
name="Vertex Group",
description="Vertex Group to assign as the masking group of this shape key",
default="",
)
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"
pose_key = get_active_pose_key(obj)
if pose_key.name:
self.sk_name = pose_key.name
return context.window_manager.invoke_props_dialog(self)
def draw(self, context):
layout = self.layout
layout.prop(self, 'sk_name')
obj = context.object
layout.prop_search(self, 'vg_name', obj, "vertex_groups")
def execute(self, context):
obj = context.object
# Ensure Basis shape key
if not obj.data.shape_keys:
basis = obj.shape_key_add()
basis.name = "Basis"
obj.data.update()
# Add new shape key
new_sk = obj.shape_key_add()
new_sk.name = self.sk_name
new_sk.value = 1
if self.vg_name:
new_sk.vertex_group = self.vg_name
pose_key = get_active_pose_key(obj)
target = pose_key.target_shapes[pose_key.active_target_shape_index]
target.name = new_sk.name
self.report({'INFO'}, f"Added shape key {new_sk.name}.")
return {'FINISHED'}
class OBJECT_OT_pose_key_clamp_influence(Operator): 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""" """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"""
@ -750,6 +675,198 @@ class OBJECT_OT_pose_key_copy_data(Operator):
return {'FINISHED'} 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
obj = context.object
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,
)
create_sk: BoolProperty(
name="Create New Shape Key",
description="Create a new blank Shape Key to push this pose into",
default=True,
)
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
create_vg: BoolProperty(
name="Create New Vertex Group",
description="Create a new blank Vertex Group as a mask for this shape key. This means the shape key won't work until this mask is authored",
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"
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'
)
row.prop(self, 'create_sk', text="", icon='ADD')
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):
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 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)
target_slot = pose_key.active_target
target_slot.name = key_block.name
self.report({'INFO'}, f"Added shape key {key_block.name}.")
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'}
def get_deforming_armature(mesh_ob: Object) -> Object | None: def get_deforming_armature(mesh_ob: Object) -> Object | None:
for mod in mesh_ob.modifiers: for mod in mesh_ob.modifiers:
if mod.type == 'ARMATURE': if mod.type == 'ARMATURE':
@ -876,9 +993,10 @@ registry = [
OBJECT_OT_pose_key_set_pose, OBJECT_OT_pose_key_set_pose,
OBJECT_OT_pose_key_push, OBJECT_OT_pose_key_push,
OBJECT_OT_pose_key_push_all, OBJECT_OT_pose_key_push_all,
OBJECT_OT_create_shape_key_for_pose,
OBJECT_OT_pose_key_clamp_influence, OBJECT_OT_pose_key_clamp_influence,
OBJECT_OT_pose_key_place_objects_in_grid, OBJECT_OT_pose_key_place_objects_in_grid,
OBJECT_OT_pose_key_jump_to_storage, OBJECT_OT_pose_key_jump_to_storage,
OBJECT_OT_pose_key_copy_data, OBJECT_OT_pose_key_copy_data,
OBJECT_OT_pose_key_shape_add,
OBJECT_OT_pose_key_shape_remove,
] ]

View File

@ -74,6 +74,10 @@ class PoseShapeKey(PropertyGroup):
active_target_shape_index: IntProperty(update=update_active_sk_index) active_target_shape_index: IntProperty(update=update_active_sk_index)
@property
def active_target(self):
return self.target_shapes[self.active_target_shape_index]
def update_name(self, context): def update_name(self, context):
if self.name == "": if self.name == "":
self.name = "Pose Key" self.name = "Pose Key"

View File

@ -129,6 +129,9 @@ class MESH_PT_shape_key_subpanel(Panel):
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
obj = context.object
if not (obj and obj.data and obj.data.shape_key_ui_type=='POSE_KEYS'):
return False
try: try:
return poll_correct_pose_key_pose(cls, context) return poll_correct_pose_key_pose(cls, context)
except AttributeError: except AttributeError:
@ -155,19 +158,21 @@ class MESH_PT_shape_key_subpanel(Panel):
class_name='POSEKEYS_UL_target_shape_keys', class_name='POSEKEYS_UL_target_shape_keys',
list_context_path=f'object.data.pose_keys[{idx}].target_shapes', list_context_path=f'object.data.pose_keys[{idx}].target_shapes',
active_idx_context_path=f'object.data.pose_keys[{idx}].active_target_shape_index', active_idx_context_path=f'object.data.pose_keys[{idx}].active_target_shape_index',
add_op_name='object.posekey_shape_add',
remove_op_name='object.posekey_shape_remove',
) )
if len(active_posekey.target_shapes) == 0: if len(active_posekey.target_shapes) == 0:
return return
active_target = active_posekey.target_shapes[active_posekey.active_target_shape_index] active_target = active_posekey.active_target
row = layout.row() row = layout.row()
if not mesh.shape_keys: if not mesh.shape_keys:
row.operator('object.create_shape_key_for_pose', icon='ADD')
return return
row.prop_search(active_target, 'shape_key_name', mesh.shape_keys, 'key_blocks') row.prop_search(active_target, 'shape_key_name', mesh.shape_keys, 'key_blocks')
if not active_target.name: if not active_target.key_block:
row.operator('object.create_shape_key_for_pose', icon='ADD', text="") add_shape_op = row.operator('object.posekey_shape_add', icon='ADD', text="")
add_shape_op.create_slot=False
sk = active_target.key_block sk = active_target.key_block
if not sk: if not sk:
return return