Rigify: Clean up "Rigify Buttons" panel UX

The overall goal of this patch is to improve the UI/UX of the panel previously known as "Rigify Buttons" which presumably takes its name from the old "Buttons Panel" which is now known as the Properties Editor.

Before:
{F10511640}
After:
{F10511624}

- Make Rigify less reliant on name matching when it comes to maintaining the link between the metarig, the UI script, the generated rig, and the widgets collection. (Use pointers only, names shouldn't matter!)
- Change the "Advanced" toggle button into a real sub-panel.
- Split up the "Rigify Buttons" panels into "Rigify Generation" and "Rigify Samples" panels in non-edit and edit mode respectively, to better describe what the user will find there.

Changes in the Rigify Buttons panel:
- Removed the "overwrite/new" enum.
	- If there is a target rig object, it will be overwritten. If not, it will be created.
	- If a rig object with the desired name already existed, but wasn't selected as the target rig, the "overwrite" option still overwrote that rig. I don't agree with that because this meant messing with data without indicating that that data is going to be messed with. Unaware users could lose data/work. With these changes, the worst thing that can happen is that your rig ends up with a .001 suffix.
- Removed the "rig name" text input field. Before this patch, this would always rename your rig object and your rig script text datablock, which I think is more frustrating than useful. Now you can simply rename them after generation yourself, and the names will be kept in subsequent generations.
- Single-column layout
- Changed the "Advanced Options" into a sub-panel instead.

On request:
- Added an info message to show the name of the successfully generated rig:
{F10159079}

Feedback welcome.

Reviewed By: angavrilov

Differential Revision: https://developer.blender.org/D11356
This commit is contained in:
Demeter Dzadik 2021-12-14 12:44:52 +01:00 committed by Demeter Dzadik
parent c60fef3817
commit ece39d809c
6 changed files with 152 additions and 185 deletions

View File

@ -512,20 +512,6 @@ def register():
IDStore.rigify_types = CollectionProperty(type=RigifyName)
IDStore.rigify_active_type = IntProperty(name="Rigify Active Type", description="The selected rig type")
bpy.types.Armature.rigify_advanced_generation = BoolProperty(name="Advanced Options",
description="Enables/disables advanced options for Rigify rig generation",
default=False)
def update_mode(self, context):
if self.rigify_generate_mode == 'new':
self.rigify_force_widget_update = False
bpy.types.Armature.rigify_generate_mode = EnumProperty(name="Rigify Generate Rig Mode",
description="'Generate Rig' mode. In 'overwrite' mode the features of the target rig will be updated as defined by the metarig. In 'new' mode a new rig will be created as defined by the metarig. Current mode",
update=update_mode,
items=( ('overwrite', 'overwrite', ''),
('new', 'new', '')))
bpy.types.Armature.rigify_force_widget_update = BoolProperty(name="Force Widget Update",
description="Forces Rigify to delete and rebuild all the rig widgets. if unset, only missing widgets will be created",
default=False)
@ -533,6 +519,9 @@ def register():
bpy.types.Armature.rigify_mirror_widgets = BoolProperty(name="Mirror Widgets",
description="Make widgets for left and right side bones linked duplicates with negative X scale for the right side, based on bone name symmetry",
default=True)
bpy.types.Armature.rigify_widgets_collection = PointerProperty(type=bpy.types.Collection,
name="Widgets Collection",
description="Defines which collection to place widget objects in. If unset, a new one will be created based on the name of the rig")
bpy.types.Armature.rigify_target_rig = PointerProperty(type=bpy.types.Object,
name="Rigify Target Rig",
@ -546,11 +535,6 @@ def register():
bpy.types.Armature.rigify_finalize_script = PointerProperty(type=bpy.types.Text,
name="Finalize Script",
description="Run this script after generation to apply user-specific changes")
bpy.types.Armature.rigify_rig_basename = StringProperty(name="Rigify Rig Name",
description="Defines the name of the Rig. If unset, in 'new' mode 'rig' will be used, in 'overwrite' mode the target rig name will be used",
default="")
IDStore.rigify_transfer_only_selected = BoolProperty(
name="Transfer Only Selected",
description="Transfer selected bones only", default=True)
@ -592,12 +576,9 @@ def unregister():
del ArmStore.rigify_colors_index
del ArmStore.rigify_colors_lock
del ArmStore.rigify_theme_to_add
del ArmStore.rigify_advanced_generation
del ArmStore.rigify_generate_mode
del ArmStore.rigify_force_widget_update
del ArmStore.rigify_target_rig
del ArmStore.rigify_rig_ui
del ArmStore.rigify_rig_basename
IDStore = bpy.types.WindowManager
del IDStore.rigify_collection

View File

@ -30,7 +30,7 @@ from .utils.widgets import WGT_PREFIX
from .utils.widgets_special import create_root_widget
from .utils.mechanism import refresh_all_drivers
from .utils.misc import gamma_correct, select_object
from .utils.collections import ensure_widget_collection, list_layer_collections, filter_layer_collections_by_object
from .utils.collections import ensure_collection, list_layer_collections, filter_layer_collections_by_object
from .utils.rig import get_rigify_type
from . import base_generate
@ -55,9 +55,6 @@ class Generator(base_generate.BaseGenerator):
self.id_store = context.window_manager
self.rig_new_name = ""
self.rig_old_name = ""
def find_rig_class(self, rig_type):
rig_module = rig_lists.rigs[rig_type]["module"]
@ -76,55 +73,42 @@ class Generator(base_generate.BaseGenerator):
self.collection = self.layer_collection.collection
def __create_rig_object(self):
scene = self.scene
id_store = self.id_store
def ensure_rig_object(self) -> bpy.types.Object:
"""Check if the generated rig already exists, so we can
regenerate in the same object. If not, create a new
object to generate the rig in.
"""
print("Fetch rig.")
meta_data = self.metarig.data
# Check if the generated rig already exists, so we can
# regenerate in the same object. If not, create a new
# object to generate the rig in.
print("Fetch rig.")
target_rig = meta_data.rigify_target_rig
if not target_rig:
if "metarig" in self.metarig.name:
rig_new_name = self.metarig.name.replace("metarig", "rig")
elif "META" in self.metarig.name:
rig_new_name = self.metarig.name.replace("META", "RIG")
else:
rig_new_name = "RIG-" + self.metarig.name
self.rig_new_name = name = meta_data.rigify_rig_basename or "rig"
obj = None
# Try existing object if overwriting
if meta_data.rigify_generate_mode == 'overwrite':
obj = meta_data.rigify_target_rig
if obj:
self.rig_old_name = obj.name
obj.name = name
obj.data.name = obj.name
elif name in bpy.data.objects:
obj = bpy.data.objects[name]
# Create a new object if not found
if not obj:
obj = bpy.data.objects.new(name, bpy.data.armatures.new(name))
obj.display_type = 'WIRE'
target_rig = bpy.data.objects.new(rig_new_name, bpy.data.armatures.new(rig_new_name))
target_rig.display_type = 'WIRE'
# If the object is already added to the scene, switch to its collection
if obj.name in self.context.scene.collection.all_objects:
self.__switch_to_usable_collection(obj)
if target_rig.name in self.context.scene.collection.all_objects:
self.__switch_to_usable_collection(target_rig)
else:
# Otherwise, add to the selected collection or the metarig collection if unusable
if (self.layer_collection not in self.usable_collections
or self.layer_collection == self.view_layer.layer_collection):
self.__switch_to_usable_collection(self.metarig, True)
self.collection.objects.link(obj)
self.collection.objects.link(target_rig)
# Configure and remember the object
meta_data.rigify_target_rig = obj
obj.data.pose_position = 'POSE'
meta_data.rigify_target_rig = target_rig
target_rig.data.pose_position = 'POSE'
self.obj = obj
return obj
return target_rig
def __unhide_rig_object(self, obj):
@ -144,11 +128,11 @@ class Generator(base_generate.BaseGenerator):
raise Exception('Could not generate: Could not find a usable collection.')
def __create_widget_group(self):
new_group_name = "WGTS_" + self.obj.name
wgts_group_name = "WGTS_" + (self.rig_old_name or self.obj.name)
# Find the old widgets collection
def __find_legacy_collection(self) -> bpy.types.Collection:
"""For backwards comp, matching by name to find a legacy collection.
(For before there was a Widget Collection PointerProperty)
"""
wgts_group_name = "WGTS_" + self.obj.name
old_collection = bpy.data.collections.get(wgts_group_name)
if not old_collection:
@ -160,16 +144,22 @@ class Generator(base_generate.BaseGenerator):
old_collection = legacy_collection
if old_collection:
# Remove widgets if force update is set
if self.metarig.data.rigify_force_widget_update:
for obj in list(old_collection.objects):
bpy.data.objects.remove(obj)
# Rename the collection
old_collection.name = new_group_name
old_collection.name = wgts_group_name
return old_collection
def ensure_widget_collection(self):
# Create/find widget collection
self.widget_collection = ensure_widget_collection(self.context, new_group_name)
self.widget_collection = self.metarig.data.rigify_widgets_collection
if not self.widget_collection:
self.widget_collection = self.__find_legacy_collection()
if not self.widget_collection:
wgts_group_name = "WGTS_" + self.obj.name.replace("RIG-", "")
self.widget_collection = ensure_collection(self.context, wgts_group_name, hidden=True)
self.metarig.data.rigify_widgets_collection = self.widget_collection
self.use_mirror_widgets = self.metarig.data.rigify_mirror_widgets
# Build tables for existing widgets
@ -177,7 +167,11 @@ class Generator(base_generate.BaseGenerator):
self.new_widget_table = {}
self.widget_mirror_mesh = {}
if not self.metarig.data.rigify_force_widget_update and self.obj.pose:
if self.metarig.data.rigify_force_widget_update:
# Remove widgets if force update is set
for obj in list(self.widget_collection.objects):
bpy.data.objects.remove(obj)
elif self.obj.pose:
# Find all widgets from the collection referenced by the old rig
known_widgets = set(obj.name for obj in self.widget_collection.objects)
@ -430,7 +424,7 @@ class Generator(base_generate.BaseGenerator):
#------------------------------------------
# Create/find the rig object and set it up
obj = self.__create_rig_object()
self.obj = obj = self.ensure_rig_object()
self.__unhide_rig_object(obj)
@ -446,8 +440,8 @@ class Generator(base_generate.BaseGenerator):
select_object(context, obj, deselect_all=True)
#------------------------------------------
# Create Group widget
self.__create_widget_group()
# Create Widget Collection
self.ensure_widget_collection()
t.tick("Create main WGTS: ")

View File

@ -1167,27 +1167,10 @@ class ScriptGenerator(base_generate.GeneratorPlugin):
layer_layout += [(l.name, l.row)]
# Generate the UI script
if metarig.data.rigify_rig_basename:
rig_ui_name = metarig.data.rigify_rig_basename + '_ui.py'
else:
rig_ui_name = 'rig_ui.py'
script = None
if metarig.data.rigify_generate_mode == 'overwrite':
script = metarig.data.rigify_rig_ui
if not script and rig_ui_name in bpy.data.texts:
script = bpy.data.texts[rig_ui_name]
if script:
script.clear()
script.name = rig_ui_name
if script is None:
script = bpy.data.texts.new(rig_ui_name)
metarig.data.rigify_rig_ui = script
script = metarig.data.rigify_rig_ui
if not script:
script = bpy.data.texts.new("rig_ui.py")
metarig.data.rigify_rig_ui = script
for s in OrderedDict.fromkeys(self.ui_imports):
script.write(s + "\n")

View File

@ -60,27 +60,27 @@ def build_type_list(context, rigify_types):
a.name = r
class DATA_PT_rigify_buttons(bpy.types.Panel):
bl_label = "Rigify Buttons"
class DATA_PT_rigify_generate(bpy.types.Panel):
bl_label = "Rigify Generation"
bl_space_type = 'PROPERTIES'
bl_region_type = 'WINDOW'
bl_context = "data"
@classmethod
def poll(cls, context):
obj = context.object
if not context.object:
return False
return context.object.type == 'ARMATURE' and context.active_object.data.get("rig_id") is None
return obj.type == 'ARMATURE' \
and obj.data.get("rig_id") is None \
and obj.mode in {'POSE', 'OBJECT'}
def draw(self, context):
C = context
layout = self.layout
obj = context.object
id_store = C.window_manager
obj = C.object
if obj.mode in {'POSE', 'OBJECT'}:
armature_id_store = C.object.data
WARNING = "Warning: Some features may change after generation"
show_warning = False
show_update_metarig = False
@ -110,7 +110,7 @@ class DATA_PT_rigify_buttons(bpy.types.Panel):
if show_warning:
layout.label(text=WARNING, icon='ERROR')
enable_generate_and_advanced = not (show_not_updatable or show_update_metarig)
enable_generate = not (show_not_updatable or show_update_metarig)
if show_not_updatable:
layout.label(text="WARNING: This metarig contains deprecated rigify rig-types and cannot be upgraded automatically.", icon='ERROR')
@ -131,71 +131,74 @@ class DATA_PT_rigify_buttons(bpy.types.Panel):
col.separator()
row = col.row()
row.operator("pose.rigify_generate", text="Generate Rig", icon='POSE_HLT')
text = "Re-Generate Rig" if obj.data.rigify_target_rig else "Generate Rig"
row.operator("pose.rigify_generate", text=text, icon='POSE_HLT')
row.enabled = enable_generate
row.enabled = enable_generate_and_advanced
if armature_id_store.rigify_advanced_generation:
icon = 'UNLOCKED'
else:
icon = 'LOCKED'
class DATA_PT_rigify_generate_advanced(bpy.types.Panel):
bl_space_type = 'PROPERTIES'
bl_region_type = 'WINDOW'
bl_context = "data"
bl_label = "Advanced"
bl_parent_id = 'DATA_PT_rigify_generate'
bl_options = {'DEFAULT_CLOSED'}
col = layout.column()
col.enabled = enable_generate_and_advanced
row = col.row()
row.prop(armature_id_store, "rigify_advanced_generation", toggle=True, icon=icon)
def draw(self, context):
layout = self.layout
layout.use_property_split = True
layout.use_property_decorate = False
if armature_id_store.rigify_advanced_generation:
armature_id_store = context.object.data
row = col.row(align=True)
row.prop(armature_id_store, "rigify_generate_mode", expand=True)
col = layout.column()
col.row().prop(armature_id_store, "rigify_target_rig", text="Target Rig")
col.row().prop(armature_id_store, "rigify_rig_ui", text="Rig UI Script")
col.separator()
col.row().prop(armature_id_store, "rigify_widgets_collection")
col.row().prop(armature_id_store, "rigify_force_widget_update")
col.row().prop(armature_id_store, "rigify_mirror_widgets")
col.separator()
col.row().prop(armature_id_store, "rigify_finalize_script", text="Run Script")
main_row = col.row(align=True).split(factor=0.3)
col1 = main_row.column()
col2 = main_row.column()
col1.label(text="Rig Name")
row = col1.row()
row.label(text="Target Rig")
row.enabled = (armature_id_store.rigify_generate_mode == "overwrite")
row = col1.row()
row.label(text="Target UI")
row.enabled = (armature_id_store.rigify_generate_mode == "overwrite")
row = col2.row(align=True)
row.prop(armature_id_store, "rigify_rig_basename", text="", icon="SORTALPHA")
class DATA_PT_rigify_samples(bpy.types.Panel):
bl_label = "Rigify Samples"
bl_space_type = 'PROPERTIES'
bl_region_type = 'WINDOW'
bl_context = "data"
row = col2.row(align=True)
row.prop(armature_id_store, "rigify_target_rig", text="")
row.enabled = (armature_id_store.rigify_generate_mode == "overwrite")
@classmethod
def poll(cls, context):
obj = context.object
if not obj:
return False
return obj.type == 'ARMATURE' \
and obj.data.get("rig_id") is None \
and obj.mode == 'EDIT'
row = col2.row()
row.prop(armature_id_store, "rigify_rig_ui", text="", icon='TEXT')
row.enabled = (armature_id_store.rigify_generate_mode == "overwrite")
def draw(self, context):
layout = self.layout
layout.use_property_split = True
layout.use_property_decorate = False
obj = context.object
id_store = context.window_manager
row = col.row()
row.prop(armature_id_store, "rigify_force_widget_update")
if armature_id_store.rigify_generate_mode == 'new':
row.enabled = False
# Build types list
build_type_list(context, id_store.rigify_types)
col.prop(armature_id_store, "rigify_mirror_widgets")
col.prop(armature_id_store, "rigify_finalize_script", text="Run Script")
if id_store.rigify_active_type > len(id_store.rigify_types):
id_store.rigify_active_type = 0
elif obj.mode == 'EDIT':
# Build types list
build_type_list(context, id_store.rigify_types)
if id_store.rigify_active_type > len(id_store.rigify_types):
id_store.rigify_active_type = 0
# Rig type list
if len(feature_set_list.get_installed_list()) > 0:
row = layout.row()
row.prop(context.object.data, "active_feature_set")
# Rig type list
if len(feature_set_list.get_installed_list()) > 0:
row = layout.row()
row.template_list("UI_UL_list", "rigify_types", id_store, "rigify_types", id_store, 'rigify_active_type')
row.prop(context.object.data, "active_feature_set")
row = layout.row()
row.template_list("UI_UL_list", "rigify_types", id_store, "rigify_types", id_store, 'rigify_active_type')
props = layout.operator("armature.metarig_sample_add", text="Add sample")
props.metarig_type = id_store.rigify_types[id_store.rigify_active_type].name
props = layout.operator("armature.metarig_sample_add", text="Add sample")
props.metarig_type = id_store.rigify_types[id_store.rigify_active_type].name
class DATA_PT_rigify_layer_names(bpy.types.Panel):
@ -791,8 +794,9 @@ class Generate(bpy.types.Operator):
return is_metarig(context.object)
def execute(self, context):
metarig = context.object
try:
generate.generate_rig(context, context.object)
generate.generate_rig(context, metarig)
except MetarigError as rig_exception:
import traceback
traceback.print_exc()
@ -803,6 +807,8 @@ class Generate(bpy.types.Operator):
traceback.print_exc()
self.report({'ERROR'}, 'Generation has thrown an exception: ' + str(rig_exception))
else:
self.report({'INFO'}, 'Successfully generated: "' + metarig.data.rigify_target_rig.name + '"')
finally:
bpy.ops.object.mode_set(mode='OBJECT')
@ -930,8 +936,10 @@ class VIEW3D_MT_rigify(bpy.types.Menu):
def draw(self, context):
layout = self.layout
obj = context.object
layout.operator(Generate.bl_idname, text="Generate")
text = "Re-Generate Rig" if obj.data.rigify_target_rig else "Generate Rig"
layout.operator(Generate.bl_idname, text=text)
if context.mode == 'EDIT_ARMATURE':
layout.separator()
@ -1381,7 +1389,9 @@ classes = (
DATA_MT_rigify_bone_groups_context_menu,
DATA_PT_rigify_bone_groups,
DATA_PT_rigify_layer_names,
DATA_PT_rigify_buttons,
DATA_PT_rigify_generate,
DATA_PT_rigify_generate_advanced,
DATA_PT_rigify_samples,
BONE_PT_rigify_buttons,
VIEW3D_PT_rigify_animation_tools,
VIEW3D_PT_tools_rigify_dev,

View File

@ -19,9 +19,6 @@
# <pep8 compliant>
import bpy
import math
from .errors import MetarigError
#=============================================
@ -65,30 +62,32 @@ def filter_layer_collections_by_object(layer_collections, obj):
return [lc for lc in layer_collections if obj in lc.collection.objects.values()]
def ensure_widget_collection(context, wgts_collection_name):
def ensure_collection(context, collection_name, hidden=False) -> bpy.types.Collection:
"""Check if a collection with a certain name exists.
If yes, return it, if not, create it in the scene root collection.
"""
view_layer = context.view_layer
layer_collection = bpy.context.layer_collection
collection = layer_collection.collection
active_layer_coll = bpy.context.layer_collection
active_collection = active_layer_coll.collection
widget_collection = bpy.data.collections.get(wgts_collection_name)
if not widget_collection:
# ------------------------------------------
# Create the widget collection
widget_collection = bpy.data.collections.new(wgts_collection_name)
widget_collection.hide_viewport = True
widget_collection.hide_render = True
collection = bpy.data.collections.get(collection_name)
if not collection:
# Create the collection
collection = bpy.data.collections.new(collection_name)
collection.hide_viewport = hidden
collection.hide_render = hidden
widget_layer_collection = None
layer_collection = None
else:
widget_layer_collection = find_layer_collection_by_collection(view_layer.layer_collection, widget_collection)
layer_collection = find_layer_collection_by_collection(view_layer.layer_collection, collection)
if not widget_layer_collection:
# Add the widget collection to the tree
collection.children.link(widget_collection)
widget_layer_collection = [c for c in layer_collection.children if c.collection == widget_collection][0]
if not layer_collection:
# Let the new collection be a child of the active one.
active_collection.children.link(collection)
layer_collection = [c for c in active_layer_coll.children if c.collection == collection][0]
widget_layer_collection.exclude = True
layer_collection.exclude = True
# Make the widget the active collection for the upcoming added (widget) objects
view_layer.active_layer_collection = widget_layer_collection
return widget_collection
# Make the new collection active.
view_layer.active_layer_collection = layer_collection
return collection

View File

@ -27,7 +27,7 @@ from mathutils import Matrix, Vector, Euler
from itertools import count
from .errors import MetarigError
from .collections import ensure_widget_collection
from .collections import ensure_collection
from .naming import change_name_side, get_name_side, Side
WGT_PREFIX = "WGT-" # Prefix for widget objects
@ -79,7 +79,7 @@ def create_widget(rig, bone_name, bone_transform_name=None, *, widget_name=None,
if generator:
collection = generator.widget_collection
else:
collection = ensure_widget_collection(bpy.context, 'WGTS_' + rig.name)
collection = ensure_collection(bpy.context, 'WGTS_' + rig.name, hidden=True)
use_mirror = generator and generator.use_mirror_widgets