diff --git a/rigify/__init__.py b/rigify/__init__.py index 181a62932..c5e6253c8 100644 --- a/rigify/__init__.py +++ b/rigify/__init__.py @@ -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 diff --git a/rigify/operators/action_layers.py b/rigify/operators/action_layers.py index 4fd7dddd0..1564c99be 100644 --- a/rigify/operators/action_layers.py +++ b/rigify/operators/action_layers.py @@ -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, ) diff --git a/rigify/rig_ui_template.py b/rigify/rig_ui_template.py index 785e03bec..7b000e186 100644 --- a/rigify/rig_ui_template.py +++ b/rigify/rig_ui_template.py @@ -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() '''