WIP: Rigify - modifier keys on Rig Layers buttons #104998

Draft
Paolo Acampora wants to merge 6 commits from PaoloAcampora/rigify-ui-improvements:panel_modifier_keys into main

When changing the target branch, be careful to rebase the branch in your fork to match. See documentation.
3 changed files with 251 additions and 3 deletions

View File

@ -699,6 +699,19 @@ def register():
id_store.rigify_types = CollectionProperty(type=RigifyName)
id_store.rigify_active_type = IntProperty(name="Rigify Active Type",
description="The selected rig type")
id_store.rigify_layers_mode = EnumProperty(
name="Layer Buttons Mode",
items=(
("OPERATOR", "Keys", "Accept key combinations with click (No drag click)", 'EVENT_SPACEKEY', 0),
("PROPERTY", "Drag", "Allow drag click (No key combinations)", 'MOUSE_LMB_DRAG', 1),
),
default="OPERATOR",
description="Choose between modifier keys or drag click"
)
id_store.rigify_layers_info = BoolProperty(
name="Display Layer Info",
default=False
)
bpy.types.Armature.rigify_force_widget_update = BoolProperty(
name="Overwrite Widget Meshes",
@ -861,6 +874,8 @@ def unregister():
del id_store.rigify_types
del id_store.rigify_active_type
del id_store.rigify_transfer_only_selected
del id_store.rigify_layers_mode
del id_store.rigify_layers_info
coll_store: typing.Any = bpy.types.BoneCollection

View File

@ -6,7 +6,8 @@ import bpy
from typing import Tuple, Optional, Sequence, Any
from bpy.types import PropertyGroup, Action, UIList, UILayout, Context, Panel, Operator, Armature
from bpy.types import (PropertyGroup, Action, UIList, UILayout, Context, Panel, Operator, Armature,
PoseBone, Bone, BoneCollection)
from bpy.props import (EnumProperty, IntProperty, BoolProperty, StringProperty, FloatProperty,
PointerProperty, CollectionProperty)
@ -178,6 +179,33 @@ def find_duplicate_slot(metarig_data: Armature, action_slot: ActionSlot) -> Opti
return None
def is_pose_bone_all_locked(pose_bone: PoseBone) -> bool:
"""Return True if all pose_bone's transform channels are locked"""
if not all(pose_bone.lock_location):
return False
if not all(pose_bone.lock_scale):
return False
if not all(pose_bone.lock_rotation):
return False
return True
def should_skip_bone(bone: Bone):
"""Return True if the bone should not be displayed (hidden, VIS_*)"""
if bone.hide:
return True
if bone.name.startswith('VIS_'):
# "VIS_*" bones are used for drawing lines, e.g. line connecting knee to IK pole
return True
return False
class BoneMultiSelect(bpy.types.PropertyGroup):
bone_name: bpy.props.StringProperty(name="bone_name")
do_select: bpy.props.BoolProperty(name="select")
# =============================================
# Operators
@ -217,9 +245,185 @@ class RIGIFY_OT_jump_to_action_slot(Operator):
return {'FINISHED'}
# noinspection PyPep8Naming
class RIGIFY_OT_display_select_group(bpy.types.Operator):
"""Toggle bone layer visibility. Shift + click to toggle selection, Ctrl + click to remove from selection.
Alt + click displays a panel"""
bl_idname = "object.rigify_display_select_bone_group"
bl_label = "Display or Select the bones that belong to a selection"
collection_name: StringProperty(default="", options={'SKIP_SAVE'})
action: EnumProperty(items=(('VIS_TOGGLE', 'TOGGLE', 'Toggle visibility'),
('SELECT', 'SELECT', 'Add to selection'),
('TOGGLE_SELECT', 'TOGGLE_SELECT', 'Toggle selection'),
('UNSELECT', 'UNSELECT', 'Remove from selection'),
('DETAIL', 'DETAIL', 'Select bones individually')),
default='VIS_TOGGLE')
@classmethod
def poll(cls, context):
return context.mode == 'POSE'
def invoke(self, context, event):
if event.shift:
self.action = 'SELECT'
elif event.ctrl:
self.action = 'UNSELECT'
elif event.alt:
self.action = 'DETAIL'
else:
self.action = 'VIS_TOGGLE'
return self.execute(context)
def execute(self, context):
if not self.collection_name:
return {'CANCELLED'}
coll = context.object.data.collections[self.collection_name]
if self.action == 'TOGGLE_SELECT':
for bone in coll.bones:
if should_skip_bone(bone):
continue
if is_pose_bone_all_locked(context.object.pose.bones[bone.name]):
continue
bone.select = not bone.select
elif self.action == 'SELECT':
for bone in coll.bones:
if should_skip_bone(bone):
continue
if is_pose_bone_all_locked(context.object.pose.bones[bone.name]):
continue
bone.select = True
elif self.action == 'UNSELECT':
for bone in coll.bones:
bone.select = False
elif self.action == 'DETAIL':
context.object.data.collections.active = coll
bpy.ops.wm.call_panel(name=RIGIFY_PT_active_group_selection.bl_idname)
else:
coll.is_visible = not coll.is_visible
return {'FINISHED'}
class RIGIFY_OT_prefix_bone_selection(bpy.types.Operator):
"""Select armature bone"""
bl_idname = "object.rigify_prefix_bone_selection"
bl_label = "Select collection bone from menu"
bl_options = {'REGISTER', 'UNDO'}
prefix: bpy.props.StringProperty(name="")
bone_multi: bpy.props.CollectionProperty(type=BoneMultiSelect, options={'SKIP_SAVE'})
def draw(self, context):
layout = self.layout
layout.operator_context = 'EXEC_REGION_WIN'
for bone_select in self.bone_multi:
layout.prop(bone_select, 'do_select', text=bone_select.bone_name, toggle=True)
def invoke(self, context, event):
self.bone_multi.clear()
bones = context.object.data.collections.active.bones
for bone in bones:
if not bone.name.startswith(self.prefix):
continue
select_bone = self.bone_multi.add()
select_bone.bone_name = bone.name
select_bone.do_select = bone.select
return context.window_manager.invoke_props_popup(self, event)
def execute(self, context):
for bone_select in self.bone_multi:
context.object.data.bones[bone_select.bone_name].select = bone_select.do_select
return {'FINISHED'}
# =============================================
# UI Panel
# noinspection PyPep8Naming
class RIGIFY_PT_active_group_selection(bpy.types.Panel):
"""Display Panel for selecting bones of the active collection"""
bl_idname = "VIEW3D_PT_active_group_selection"
bl_label = "Select Bone"
bl_options = {'INSTANCED'}
bl_space_type = 'VIEW_3D'
bl_region_type = 'WINDOW'
@classmethod
def poll(cls, context):
return context.mode == 'POSE'
@staticmethod
def collect_bone_sides(collection: BoneCollection):
left_bones = []
mid_bones = []
right_bones = []
prefix_bones = set()
digits = ".0123456789" # used for stripping dot and number from bone names
armature = collection.id_data
for bone in collection.bones:
if should_skip_bone(bone):
continue
if bone.name[-1].isdigit():
# Looking for bones like "*.L.015", "*.R.023", "*.001"
base_name = bone.name.rstrip(digits)
if base_name in armature.bones:
prefix_bones.add(base_name)
continue
if bone.name.endswith('.L'):
left_bones.append(bone)
continue
if bone.name.endswith('.R'):
right_bones.append(bone)
else:
mid_bones.append(bone)
return (left_bones, right_bones, mid_bones), prefix_bones
def draw(self, context):
collection = context.object.data.collections.active
bone_lists, prefix_bones = self.collect_bone_sides(collection)
# display Left and Right bones
row = self.layout.row()
columns = row.column(), row.column()
pose_bones = context.object.pose.bones
for col, bone_list in zip(columns, bone_lists[:2]):
for bone in bone_list:
if is_pose_bone_all_locked(pose_bones[bone.name]):
continue
if bone.name in prefix_bones:
col.operator(RIGIFY_OT_prefix_bone_selection.bl_idname, text=bone.name, icon='TRIA_RIGHT').prefix = bone.name
else:
col.prop(bone, 'select', text=bone.name, toggle=True, expand=True)
col = self.layout.column()
# display Mid bones
for bone in bone_lists[2]:
if is_pose_bone_all_locked(pose_bones[bone.name]):
continue
if bone.name in prefix_bones:
col.operator(RIGIFY_OT_prefix_bone_selection.bl_idname, text=bone.name, icon='TRIA_RIGHT').prefix = bone.name
else:
col.prop(bone, 'select', text=bone.name, toggle=True, expand=True)
# noinspection PyPep8Naming
class RIGIFY_UL_action_slots(UIList):
def draw_item(self, context: Context, layout: UILayout, data: Armature,
@ -499,8 +703,12 @@ class DATA_PT_rigify_actions(Panel):
classes = (
ActionSlot,
BoneMultiSelect,
RIGIFY_OT_action_create,
RIGIFY_OT_jump_to_action_slot,
RIGIFY_OT_display_select_group,
RIGIFY_OT_prefix_bone_selection,
RIGIFY_PT_active_group_selection,
RIGIFY_UL_action_slots,
DATA_PT_rigify_actions,
)

View File

@ -10,6 +10,8 @@ from typing import Union, Optional, Any
from .utils.animation import SCRIPT_REGISTER_BAKE, SCRIPT_UTILITIES_BAKE
from .utils.mechanism import quote_property
from .operators.action_layers import RIGIFY_OT_display_select_group
from . import base_generate
from rna_prop_ui import rna_idprop_quote_path
@ -884,7 +886,7 @@ class RigBakeSettings(bpy.types.Panel):
'''
UI_LAYERS_PANEL = '''
UI_LAYERS_PANEL = f'''
class RigLayers(bpy.types.Panel):
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
@ -901,6 +903,26 @@ class RigLayers(bpy.types.Panel):
def draw(self, context):
layout = self.layout
wm = context.window_manager
split = layout.split()
row = split.row()
row.prop(wm, 'rigify_layers_mode', expand=True, text=None if wm.rigify_layers_info else "")
row.prop(wm, 'rigify_layers_info', icon='INFO', text="")
if wm.rigify_layers_info:
box = layout.box()
box.label(text="Click: toggle Layer visibility")
if wm.rigify_layers_mode == 'PROPERTY':
box.label(text="Click + Drag: multi-toggle")
else:
box.label(text="Click + Shift: Select Bones")
box.label(text="Click + Ctrl: Unselect Bones")
box.label(text="Click + Alt: Pick Bones")
box.label(text="No drag click", icon='ERROR')
layout.separator()
row_table = collections.defaultdict(list)
for coll in context.active_object.data.collections:
row_id = coll.get('rigify_ui_row', 0)
@ -913,7 +935,10 @@ class RigLayers(bpy.types.Panel):
if row_buttons:
for coll in row_buttons:
title = coll.get('rigify_ui_title') or coll.name
row.prop(coll, 'is_visible', toggle=True, text=title)
if wm.rigify_layers_mode == 'PROPERTY':
row.prop(coll, 'is_visible', toggle=True, text=title)
else:
row.operator('{RIGIFY_OT_display_select_group.bl_idname}', text=title, depress=coll.is_visible).collection_name = coll.name
else:
row.separator()
'''