Pose Shape Keys: Major Update #321
@ -1,12 +1,12 @@
|
||||
import bpy
|
||||
from bpy.types import Object, Operator
|
||||
from bpy.props import StringProperty
|
||||
from bpy.props import StringProperty, BoolProperty
|
||||
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
|
||||
from .ui_list import UILIST_OT_Entry_Add, UILIST_OT_Entry_Remove
|
||||
|
||||
# When saving or pushing shapes, disable any modifier NOT in this list.
|
||||
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)
|
||||
|
||||
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:
|
||||
self.layout.alert = True
|
||||
self.layout.label(text="Name cannot be empty.", icon='ERROR')
|
||||
layout.alert = True
|
||||
layout.label(text="Name cannot be empty.", icon='ERROR')
|
||||
|
||||
def execute(self, context):
|
||||
if not self.pose_key_name:
|
||||
@ -482,84 +485,6 @@ class OBJECT_OT_pose_key_push_all(Operator, OperatorWithWarning, SaveAndRestoreS
|
||||
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):
|
||||
"""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'}
|
||||
|
||||
|
||||
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:
|
||||
for mod in mesh_ob.modifiers:
|
||||
if mod.type == 'ARMATURE':
|
||||
@ -876,9 +993,10 @@ registry = [
|
||||
OBJECT_OT_pose_key_set_pose,
|
||||
OBJECT_OT_pose_key_push,
|
||||
OBJECT_OT_pose_key_push_all,
|
||||
OBJECT_OT_create_shape_key_for_pose,
|
||||
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,
|
||||
]
|
||||
|
@ -74,6 +74,10 @@ class PoseShapeKey(PropertyGroup):
|
||||
|
||||
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):
|
||||
if self.name == "":
|
||||
self.name = "Pose Key"
|
||||
|
@ -129,6 +129,9 @@ class MESH_PT_shape_key_subpanel(Panel):
|
||||
|
||||
@classmethod
|
||||
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:
|
||||
return poll_correct_pose_key_pose(cls, context)
|
||||
except AttributeError:
|
||||
@ -155,19 +158,21 @@ class MESH_PT_shape_key_subpanel(Panel):
|
||||
class_name='POSEKEYS_UL_target_shape_keys',
|
||||
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',
|
||||
add_op_name='object.posekey_shape_add',
|
||||
remove_op_name='object.posekey_shape_remove',
|
||||
)
|
||||
|
||||
if len(active_posekey.target_shapes) == 0:
|
||||
return
|
||||
|
||||
active_target = active_posekey.target_shapes[active_posekey.active_target_shape_index]
|
||||
active_target = active_posekey.active_target
|
||||
row = layout.row()
|
||||
if not mesh.shape_keys:
|
||||
row.operator('object.create_shape_key_for_pose', icon='ADD')
|
||||
return
|
||||
row.prop_search(active_target, 'shape_key_name', mesh.shape_keys, 'key_blocks')
|
||||
if not active_target.name:
|
||||
row.operator('object.create_shape_key_for_pose', icon='ADD', text="")
|
||||
if not active_target.key_block:
|
||||
add_shape_op = row.operator('object.posekey_shape_add', icon='ADD', text="")
|
||||
add_shape_op.create_slot=False
|
||||
sk = active_target.key_block
|
||||
if not sk:
|
||||
return
|
||||
|
Loading…
Reference in New Issue
Block a user