2025-12-01
This commit is contained in:
@@ -0,0 +1,305 @@
|
||||
# SPDX-FileCopyrightText: 2025 Blender Studio Tools Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import bpy
|
||||
from bpy.types import Panel, UIList, Menu
|
||||
from bl_ui.properties_data_mesh import DATA_PT_shape_keys
|
||||
from bpy.props import EnumProperty
|
||||
|
||||
from .ui_list import draw_ui_list
|
||||
from .ops import get_deforming_armature, poll_correct_pose_key_pose, get_active_pose_key
|
||||
from .prefs import get_addon_prefs
|
||||
from .props import update_posekey_index
|
||||
|
||||
|
||||
class MESH_PT_pose_keys(Panel):
|
||||
bl_space_type = 'PROPERTIES'
|
||||
bl_region_type = 'WINDOW'
|
||||
bl_context = 'data'
|
||||
bl_options = {'DEFAULT_CLOSED'}
|
||||
bl_label = "Pose Shape Keys"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.object and context.object.type == 'MESH'
|
||||
|
||||
def draw(self, context):
|
||||
obj = context.object
|
||||
mesh = obj.data
|
||||
layout = self.layout.column()
|
||||
|
||||
layout.row().prop(mesh, 'shape_key_ui_type', expand=True)
|
||||
|
||||
if mesh.shape_key_ui_type == 'DEFAULT':
|
||||
return DATA_PT_shape_keys.draw(self, context)
|
||||
|
||||
arm_ob = get_deforming_armature(obj)
|
||||
if not arm_ob:
|
||||
layout.alert = True
|
||||
layout.label(text="Object must be deformed by an Armature to use Pose Keys.")
|
||||
return
|
||||
|
||||
if mesh.shape_keys and not mesh.shape_keys.use_relative:
|
||||
layout.alert = True
|
||||
layout.label("Relative Shape Keys must be enabled!")
|
||||
return
|
||||
|
||||
list_row = layout.row()
|
||||
|
||||
groups_col = list_row.column()
|
||||
draw_ui_list(
|
||||
groups_col,
|
||||
context,
|
||||
class_name='POSEKEYS_UL_pose_keys',
|
||||
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
|
||||
layout.use_property_decorate = False
|
||||
|
||||
if len(mesh.pose_keys) == 0:
|
||||
return
|
||||
|
||||
active_posekey = get_active_pose_key(context.object)
|
||||
if not active_posekey:
|
||||
return
|
||||
|
||||
action_split = layout.row().split(factor=0.4, align=True)
|
||||
action_split.alignment = 'RIGHT'
|
||||
action_split.label(text="Action")
|
||||
row = action_split.row(align=True)
|
||||
icon = 'FORWARD'
|
||||
if active_posekey.action:
|
||||
icon = 'FILE_REFRESH'
|
||||
row.operator('object.posekey_auto_init', text="", icon=icon)
|
||||
row.prop(active_posekey, 'action', text="")
|
||||
layout.prop(active_posekey, 'frame')
|
||||
|
||||
layout.separator()
|
||||
|
||||
layout.operator('object.posekey_set_pose', text="Set Pose", icon="ARMATURE_DATA")
|
||||
|
||||
layout.separator()
|
||||
|
||||
row = layout.row(align=True)
|
||||
text = "Save Posed Mesh"
|
||||
if active_posekey.storage_object:
|
||||
text = "Overwrite Posed Mesh"
|
||||
row.operator('object.posekey_save', text=text, icon="FILE_TICK")
|
||||
row.prop(active_posekey, 'storage_object', text="")
|
||||
row.operator('object.posekey_jump_to_storage', text="", icon='RESTRICT_SELECT_OFF')
|
||||
|
||||
|
||||
class POSEKEYS_UL_pose_keys(UIList):
|
||||
def draw_item(self, context, layout, data, item, _icon, _active_data, _active_propname):
|
||||
pose_key = item
|
||||
|
||||
if self.layout_type != 'DEFAULT':
|
||||
# Other layout types not supported by this UIList.
|
||||
return
|
||||
|
||||
split = layout.row().split(factor=0.7, align=True)
|
||||
|
||||
icon = 'SURFACE_NCIRCLE' if pose_key.storage_object else 'CURVE_NCIRCLE'
|
||||
name_row = split.row()
|
||||
if pose_key.has_error:
|
||||
# name_row.alert = True
|
||||
icon = 'ERROR'
|
||||
if not pose_key.name:
|
||||
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)
|
||||
|
||||
|
||||
class MESH_MT_pose_key_utils(Menu):
|
||||
bl_label = "Pose Key Utilities"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.operator('object.posekey_object_grid', icon='LIGHTPROBE_VOLUME')
|
||||
layout.operator('object.posekey_push_all', icon='WORLD')
|
||||
layout.operator('object.posekey_clamp_influence', icon='NORMALIZE_FCURVES')
|
||||
layout.operator('object.posekey_copy_data', icon='PASTEDOWN')
|
||||
|
||||
|
||||
class MESH_PT_shape_key_subpanel(Panel):
|
||||
bl_space_type = 'PROPERTIES'
|
||||
bl_region_type = 'WINDOW'
|
||||
bl_context = 'data'
|
||||
bl_options = {'DEFAULT_CLOSED'}
|
||||
bl_label = "Shape Key Slots"
|
||||
bl_parent_id = "MESH_PT_pose_keys"
|
||||
|
||||
@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, demand_pose=False)
|
||||
except AttributeError:
|
||||
# Happens any time that function tries to set a poll message,
|
||||
# since panels don't have poll messages, lol.
|
||||
return False
|
||||
|
||||
def draw(self, context):
|
||||
obj = context.object
|
||||
mesh = obj.data
|
||||
layout = self.layout
|
||||
|
||||
layout.use_property_split = True
|
||||
layout.use_property_decorate = False
|
||||
|
||||
idx = context.object.data.active_pose_key_index
|
||||
active_posekey = get_active_pose_key(context.object)
|
||||
if not active_posekey:
|
||||
return
|
||||
|
||||
layout.operator('object.posekey_push', text="Overwrite Shape Keys", icon="IMPORT")
|
||||
|
||||
draw_ui_list(
|
||||
layout,
|
||||
context,
|
||||
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.active_target
|
||||
selector_row = layout.row()
|
||||
if not mesh.shape_keys:
|
||||
return
|
||||
icon = 'SHAPEKEY_DATA'
|
||||
key_block = context.object.data.shape_keys.key_blocks.get(active_target.shape_key_name)
|
||||
if not key_block:
|
||||
# selector_row.alert = True
|
||||
icon = 'ERROR'
|
||||
selector_row.prop_search(active_target, 'shape_key_name', mesh.shape_keys, 'key_blocks', icon=icon)
|
||||
if not active_target.key_block:
|
||||
add_shape_op = selector_row.operator('object.posekey_shape_add', icon='ADD', text="")
|
||||
add_shape_op.create_slot=False
|
||||
sk = active_target.key_block
|
||||
if not sk:
|
||||
return
|
||||
addon_prefs = get_addon_prefs(context)
|
||||
icon = 'HIDE_OFF' if addon_prefs.show_shape_key_info else 'HIDE_ON'
|
||||
selector_row.prop(addon_prefs, 'show_shape_key_info', text="", icon=icon)
|
||||
if addon_prefs.show_shape_key_info:
|
||||
layout.prop(active_target, 'mirror_x')
|
||||
split = layout.split(factor=0.1)
|
||||
split.row()
|
||||
col = split.column()
|
||||
col.row().prop(sk, 'value')
|
||||
row = col.row(align=True)
|
||||
row.prop(sk, 'slider_min', text="Range")
|
||||
row.prop(sk, 'slider_max', text="")
|
||||
col.prop_search(sk, "vertex_group", obj, "vertex_groups", text="Vertex Mask")
|
||||
col.row().prop(sk, 'relative_key')
|
||||
|
||||
|
||||
class POSEKEYS_UL_target_shape_keys(UIList):
|
||||
def draw_item(self, context, layout, data, item, _icon, _active_data, _active_propname):
|
||||
obj = context.object
|
||||
pose_key_target = item
|
||||
key_block = pose_key_target.key_block
|
||||
|
||||
if self.layout_type != 'DEFAULT':
|
||||
# Other layout types not supported by this UIList.
|
||||
return
|
||||
|
||||
split = layout.row().split(factor=0.7, align=True)
|
||||
|
||||
name_row = split.row()
|
||||
icon = 'SHAPEKEY_DATA'
|
||||
if pose_key_target.has_error:
|
||||
icon = 'ERROR'
|
||||
name_row.prop(pose_key_target, 'name', text="", emboss=False, icon=icon)
|
||||
|
||||
value_row = split.row(align=True)
|
||||
value_row.emboss = 'NONE_OR_STATUS'
|
||||
if not key_block:
|
||||
return
|
||||
if (
|
||||
key_block.mute
|
||||
or (obj.mode == 'EDIT' and not (obj.use_shape_key_edit_mode and obj.type == 'MESH'))
|
||||
or (obj.show_only_shape_key and key_block != obj.active_shape_key)
|
||||
):
|
||||
name_row.active = value_row.active = False
|
||||
|
||||
value_row.operator('object.posekey_magic_driver', text="", icon='DECORATE_DRIVER').shapekey_name = key_block.name
|
||||
value_row.prop(key_block, "value", text="")
|
||||
|
||||
mute_row = split.row()
|
||||
mute_row.alignment = 'RIGHT'
|
||||
mute_row.prop(key_block, 'mute', emboss=False, text="")
|
||||
|
||||
|
||||
@classmethod
|
||||
def shape_key_panel_new_poll(cls, context):
|
||||
engine = context.engine
|
||||
obj = context.object
|
||||
return obj and obj.type in {'LATTICE', 'CURVE', 'SURFACE'} and (engine in cls.COMPAT_ENGINES)
|
||||
|
||||
|
||||
registry = [
|
||||
POSEKEYS_UL_pose_keys,
|
||||
POSEKEYS_UL_target_shape_keys,
|
||||
MESH_PT_pose_keys,
|
||||
MESH_PT_shape_key_subpanel,
|
||||
MESH_MT_pose_key_utils,
|
||||
]
|
||||
|
||||
|
||||
def register():
|
||||
bpy.types.Mesh.shape_key_ui_type = EnumProperty(
|
||||
name="Shape Key List Type",
|
||||
items=[
|
||||
('DEFAULT', 'Shape Keys', "Show a flat list of shape keys"),
|
||||
(
|
||||
'POSE_KEYS',
|
||||
'Pose Shape Keys',
|
||||
"Organize shape keys into a higher-level concept called Pose Keys. These can store vertex positions and push one shape to multiple shape keys at once, relative to existing deformation",
|
||||
),
|
||||
],
|
||||
update=update_posekey_index,
|
||||
)
|
||||
|
||||
for panel in bpy.types.Panel.__subclasses__():
|
||||
if hasattr(panel, 'bl_parent_id') and panel.bl_parent_id == 'DATA_PT_shape_keys':
|
||||
panel.bl_parent_id = 'MESH_PT_pose_keys'
|
||||
try:
|
||||
bpy.utils.unregister_class(panel)
|
||||
bpy.utils.register_class(panel)
|
||||
except RuntimeError:
|
||||
# Class was already unregistered, leave it unregistered.
|
||||
pass
|
||||
|
||||
DATA_PT_shape_keys.replacement = 'MESH_PT_pose_keys' # This is used by GeoNodeShapeKeys add-on to register to the correct parent panel. Could be used by any other add-on I guess.
|
||||
DATA_PT_shape_keys.old_poll = DATA_PT_shape_keys.poll
|
||||
DATA_PT_shape_keys.poll = shape_key_panel_new_poll
|
||||
|
||||
|
||||
def unregister():
|
||||
for panel in bpy.types.Panel.__subclasses__():
|
||||
if hasattr(panel, 'bl_parent_id') and panel.bl_parent_id == 'MESH_PT_pose_keys':
|
||||
panel.bl_parent_id = 'DATA_PT_shape_keys'
|
||||
try:
|
||||
bpy.utils.unregister_class(panel)
|
||||
bpy.utils.register_class(panel)
|
||||
except RuntimeError:
|
||||
# Class was already unregistered, leave it unregistered.
|
||||
pass
|
||||
|
||||
del bpy.types.Mesh.shape_key_ui_type
|
||||
DATA_PT_shape_keys.poll = DATA_PT_shape_keys.old_poll
|
||||
DATA_PT_shape_keys.replacement = None
|
||||
Reference in New Issue
Block a user