1 # SPDX-License-Identifier: GPL-2.0-or-later
6 "author": "Nathan Vegdahl, Lucio Rossi, Ivan Cappiello, Alexander Gavrilov", # noqa
8 "description": "Automatic rigging from building-block components",
9 "location": "Armature properties, Bone properties, View3d tools panel, Armature Add menu",
10 "doc_url": "{BLENDER_MANUAL_URL}/addons/rigging/rigify/index.html",
11 "category": "Rigging",
20 # The order in which core modules of the addon are loaded and reloaded.
21 # Modules not in this list are removed from memory upon reload.
22 # With the sole exception of 'utils', modules must be listed in the
23 # correct dependency order.
24 initial_load_order
= [
33 'utils.widgets_basic',
34 'utils.widgets_special',
48 'utils.action_layers',
56 def get_loaded_modules():
57 prefix
= __name__
+ '.'
58 return [name
for name
in sys
.modules
if name
.startswith(prefix
)]
62 fixed_modules
= set(reload_list
)
64 for name
in get_loaded_modules():
65 if name
not in fixed_modules
:
68 for name
in reload_list
:
69 importlib
.reload(sys
.modules
[name
])
72 def compare_module_list(a
: list[str], b
: list[str]):
73 # HACK: ignore the "utils" module when comparing module load orders,
74 # because it is inconsistent for reasons unknown.
75 # See rBAa918332cc3f821f5a70b1de53b65dd9ca596b093.
76 utils_module_name
= __name__
+ '.utils'
78 a_copy
.remove(utils_module_name
)
80 b_copy
.remove(utils_module_name
)
81 return a_copy
== b_copy
84 def load_initial_modules() -> list[str]:
85 names
= [__name__
+ '.' + name
for name
in initial_load_order
]
87 for i
, name
in enumerate(names
):
88 importlib
.import_module(name
)
90 module_list
= get_loaded_modules()
91 expected_list
= names
[0: max(11, i
+1)]
93 if not compare_module_list(module_list
, expected_list
):
94 print(f
'!!! RIGIFY: initial load order mismatch after {name} - expected: \n',
95 expected_list
, '\nGot:\n', module_list
)
101 rig_lists
.get_internal_rigs()
102 metarig_menu
.init_metarig_menu()
105 if "reload_list" in locals():
108 load_list
= load_initial_modules()
110 from . import (utils
, base_rig
, base_generate
, rig_ui_template
, feature_set_list
, rig_lists
,
111 generate
, ui
, metarig_menu
, operators
)
113 reload_list
= reload_list_init
= get_loaded_modules()
115 if not compare_module_list(reload_list
, load_list
):
116 print('!!! RIGIFY: initial load order mismatch - expected: \n',
117 load_list
, '\nGot:\n', reload_list
)
122 from bpy
.types
import AddonPreferences
# noqa: E402
123 from bpy
.props
import ( # noqa: E402
135 """Returns the currently active generator instance."""
136 return base_generate
.BaseGenerator
.instance
139 class RigifyFeatureSets(bpy
.types
.PropertyGroup
):
140 name
: bpy
.props
.StringProperty()
141 module_name
: bpy
.props
.StringProperty()
143 def toggle_feature_set(self
, context
):
144 feature_set_list
.call_register_function(self
.module_name
, self
.enabled
)
145 RigifyPreferences
.get_instance(context
).update_external_rigs()
147 enabled
: bpy
.props
.BoolProperty(
149 description
="Whether this feature-set is registered or not",
150 update
=toggle_feature_set
,
155 # noinspection PyPep8Naming
156 class RIGIFY_UL_FeatureSets(bpy
.types
.UIList
):
157 def draw_item(self
, context
, layout
, data
, item
, icon
, active_data
, active_propname
, _index
=0, _flag
=0):
158 # rigify_prefs: RigifyPreferences = data
159 # feature_sets = rigify_prefs.rigify_feature_sets
160 # active_set: RigifyFeatureSets = feature_sets[rigify_prefs.active_feature_set_index]
161 feature_set_entry
: RigifyFeatureSets
= item
162 if self
.layout_type
in {'DEFAULT', 'COMPACT'}:
164 row
.prop(feature_set_entry
, 'name', text
="", emboss
=False)
166 icon
= 'CHECKBOX_HLT' if feature_set_entry
.enabled
else 'CHECKBOX_DEHLT'
167 row
.enabled
= feature_set_entry
.enabled
168 layout
.prop(feature_set_entry
, 'enabled', text
="", icon
=icon
, emboss
=False)
169 elif self
.layout_type
in {'GRID'}:
173 class RigifyPreferences(AddonPreferences
):
174 # this must match the addon name, use '__package__'
175 # when defining this in a submodule of a python package.
179 def get_instance(context
: bpy
.types
.Context
= None) -> 'RigifyPreferences':
180 prefs
= (context
or bpy
.context
).preferences
.addons
[__package__
].preferences
181 assert isinstance(prefs
, RigifyPreferences
)
184 def register_feature_sets(self
, do_register
: bool):
185 """Call register or unregister of external feature sets"""
186 self
.refresh_installed_feature_sets()
187 for set_name
in feature_set_list
.get_enabled_modules_names():
188 feature_set_list
.call_register_function(set_name
, do_register
)
190 def refresh_installed_feature_sets(self
):
191 """Synchronize preferences entries with what's actually in the file system."""
192 feature_set_prefs
= self
.rigify_feature_sets
194 module_names
= feature_set_list
.get_installed_modules_names()
196 # If there is a feature set preferences entry with no corresponding
197 # installed module, user must've manually removed it from the filesystem,
198 # so let's remove such entries.
199 to_delete
= [i
for i
, fs
in enumerate(feature_set_prefs
)
200 if fs
.module_name
not in module_names
]
201 for i
in reversed(to_delete
):
202 feature_set_prefs
.remove(i
)
204 # If there is an installed feature set in the file system but no corresponding
205 # entry, user must've installed it manually. Make sure it has an entry.
206 for module_name
in module_names
:
207 for fs
in feature_set_prefs
:
208 if module_name
== fs
.module_name
:
211 fs
= feature_set_prefs
.add()
212 fs
.name
= feature_set_list
.get_ui_name(module_name
)
213 fs
.module_name
= module_name
215 def update_external_rigs(self
):
216 """Get external feature sets"""
218 self
.refresh_installed_feature_sets()
220 set_list
= feature_set_list
.get_enabled_modules_names()
223 print('Reloading external rigs...')
224 rig_lists
.get_external_rigs(set_list
)
227 print('Reloading external metarigs...')
228 metarig_menu
.get_external_metarigs(set_list
)
230 # Re-register rig parameters
231 register_rig_parameters()
233 rigify_feature_sets
: bpy
.props
.CollectionProperty(type=RigifyFeatureSets
)
234 active_feature_set_index
: IntProperty()
236 def draw(self
, context
: bpy
.types
.Context
):
237 layout
: bpy
.types
.UILayout
= self
.layout
239 layout
.label(text
="Feature Sets:")
241 layout
.operator("wm.rigify_add_feature_set", text
="Install Feature Set from File...", icon
='FILEBROWSER')
245 'RIGIFY_UL_FeatureSets',
247 self
, "rigify_feature_sets",
248 self
, 'active_feature_set_index'
251 # Clamp active index to ensure it is in bounds.
252 self
.active_feature_set_index
= max(0, min(self
.active_feature_set_index
, len(self
.rigify_feature_sets
)-1))
254 if len(self
.rigify_feature_sets
) > 0:
255 active_fs
= self
.rigify_feature_sets
[self
.active_feature_set_index
]
258 draw_feature_set_prefs(layout
, context
, active_fs
)
261 def draw_feature_set_prefs(layout
: bpy
.types
.UILayout
, _context
, feature_set
: RigifyFeatureSets
):
262 info
= feature_set_list
.get_info_dict(feature_set
.module_name
)
264 description
= feature_set
.name
265 if 'description' in info
:
266 description
= info
['description']
268 col
= layout
.column()
271 split
= col
.row().split(factor
=split_factor
)
272 split
.label(text
="Description:")
273 split
.label(text
=description
)
275 mod
= feature_set_list
.get_module_safe(feature_set
.module_name
)
277 split
= col
.row().split(factor
=split_factor
)
278 split
.label(text
="File:")
279 split
.label(text
=mod
.__file
__, translate
=False)
282 split
= col
.row().split(factor
=split_factor
)
283 split
.label(text
="Author:")
284 split
.label(text
=info
["author"])
286 if 'version' in info
:
287 split
= col
.row().split(factor
=split_factor
)
288 split
.label(text
="Version:")
289 split
.label(text
=".".join(str(x
) for x
in info
['version']), translate
=False)
290 if 'warning' in info
:
291 split
= col
.row().split(factor
=split_factor
)
292 split
.label(text
="Warning:")
293 split
.label(text
=" " + info
['warning'], icon
='ERROR')
295 split
= col
.row().split(factor
=split_factor
)
296 split
.label(text
="Internet:")
299 op
= row
.operator('wm.url_open', text
="Repository", icon
='URL')
300 op
.url
= info
['link']
301 if 'doc_url' in info
:
302 op
= row
.operator('wm.url_open', text
="Documentation", icon
='HELP')
303 op
.url
= info
['doc_url']
304 if 'tracker_url' in info
:
305 op
= row
.operator('wm.url_open', text
="Report a Bug", icon
='URL')
306 op
.url
= info
['tracker_url']
308 row
.operator("wm.rigify_remove_feature_set", text
="Remove", icon
='CANCEL')
311 class RigifyName(bpy
.types
.PropertyGroup
):
312 name
: StringProperty()
315 class RigifyColorSet(bpy
.types
.PropertyGroup
):
316 name
: StringProperty(name
="Color Set", default
=" ")
317 active
: FloatVectorProperty(
320 default
=(1.0, 1.0, 1.0),
322 description
="color picker"
324 normal
: FloatVectorProperty(
327 default
=(1.0, 1.0, 1.0),
329 description
="color picker"
331 select
: FloatVectorProperty(
334 default
=(1.0, 1.0, 1.0),
336 description
="color picker"
338 standard_colors_lock
: BoolProperty(default
=True)
341 class RigifySelectionColors(bpy
.types
.PropertyGroup
):
342 select
: FloatVectorProperty(
345 default
=(0.314, 0.784, 1.0),
347 description
="color picker"
350 active
: FloatVectorProperty(
353 default
=(0.549, 1.0, 1.0),
355 description
="color picker"
359 class RigifyParameters(bpy
.types
.PropertyGroup
):
360 name
: StringProperty()
363 # Parameter update callback
368 def update_callback(prop_name
):
369 from .utils
.rig
import get_rigify_type
371 def callback(params
, context
):
373 # Do not recursively call if the callback updates other parameters
377 bone
= context
.active_pose_bone
379 if bone
and bone
.rigify_parameters
== params
:
380 rig_info
= rig_lists
.rigs
.get(get_rigify_type(bone
), None)
382 rig_cb
= getattr(rig_info
["module"].Rig
, 'on_parameter_update', None)
384 rig_cb(context
, bone
, params
, prop_name
)
391 # Remember the initial property set
392 RIGIFY_PARAMETERS_BASE_DIR
= set(dir(RigifyParameters
))
393 RIGIFY_PARAMETER_TABLE
= {'name': ('DEFAULT', StringProperty())}
396 def clear_rigify_parameters():
397 for name
in list(dir(RigifyParameters
)):
398 if name
not in RIGIFY_PARAMETERS_BASE_DIR
:
399 delattr(RigifyParameters
, name
)
400 if name
in RIGIFY_PARAMETER_TABLE
:
401 del RIGIFY_PARAMETER_TABLE
[name
]
404 def format_property_spec(spec
):
405 """Turns the return value of bpy.props.SomeProperty(...) into a readable string."""
406 callback
, params
= spec
407 param_str
= ["%s=%r" % (k
, v
) for k
, v
in params
.items()]
408 return "%s(%s)" % (callback
.__name
__, ', '.join(param_str
))
411 class RigifyParameterValidator(object):
413 A wrapper around RigifyParameters that verifies properties
414 defined from rigs for incompatible redefinitions using a table.
416 Relies on the implementation details of bpy.props.* return values:
417 specifically, they just return a tuple containing the real define
418 function, and a dictionary with parameters. This allows comparing
419 parameters before the property is actually defined.
425 def __init__(self
, params
, rig_name
, prop_table
):
426 self
.__params
= params
427 self
.__rig
_name
= rig_name
428 self
.__prop
_table
= prop_table
430 def __getattr__(self
, name
):
431 return getattr(self
.__params
, name
)
433 def __setattr__(self
, name
, val_original
):
434 # allow __init__ to work correctly
435 if hasattr(RigifyParameterValidator
, name
):
436 return object.__setattr
__(self
, name
, val_original
)
438 if not isinstance(val_original
, bpy
.props
._PropertyDeferred
): # noqa
439 print(f
"!!! RIGIFY RIG {self.__rig_name}: "
440 f
"INVALID DEFINITION FOR RIG PARAMETER {name}: {repr(val_original)}\n")
443 # actually defining the property modifies the dictionary with new parameters, so copy it now
444 val
= (val_original
.function
, val_original
.keywords
)
445 new_def
= (val
[0], val
[1].copy())
447 if 'poll' in new_def
[1]:
448 del new_def
[1]['poll']
450 if name
in self
.__prop
_table
:
451 cur_rig
, cur_info
= self
.__prop
_table
[name
]
452 if new_def
!= cur_info
:
453 print(f
"!!! RIGIFY RIG {self.__rig_name}: REDEFINING PARAMETER {name} AS:\n\n"
454 f
" {format_property_spec(val)}\n"
455 f
"!!! PREVIOUS DEFINITION BY {cur_rig}:\n\n"
456 f
" {format_property_spec(cur_info)}\n")
458 # inject a generic update callback that calls the appropriate rig class method
459 if val
[0] != bpy
.props
.CollectionProperty
:
460 val
[1]['update'] = update_callback(name
)
462 setattr(self
.__params
, name
, val_original
)
463 self
.__prop
_table
[name
] = (self
.__rig
_name
, new_def
)
466 class RigifyArmatureLayer(bpy
.types
.PropertyGroup
):
468 if 'group_prop' in self
.keys():
469 return self
['group_prop']
473 def set_group(self
, value
):
474 arm
= utils
.misc
.verify_armature_obj(bpy
.context
.object).data
475 colors
= utils
.rig
.get_rigify_colors(arm
)
476 if value
> len(colors
):
477 self
['group_prop'] = len(colors
)
479 self
['group_prop'] = value
481 name
: StringProperty(name
="Layer Name", default
=" ")
482 row
: IntProperty(name
="Layer Row", default
=1, min=1, max=32,
483 description
='UI row for this layer')
484 # noinspection SpellCheckingInspection
485 selset
: BoolProperty(name
="Selection Set", default
=False,
486 description
='Add Selection Set for this layer')
487 group
: IntProperty(name
="Bone Group", default
=0, min=0, max=32,
488 get
=get_group
, set=set_group
,
489 description
='Assign Bone Group to this layer')
499 RigifySelectionColors
,
501 RIGIFY_UL_FeatureSets
,
508 from bpy
.utils
import register_class
512 feature_set_list
.register()
513 metarig_menu
.register()
521 bpy
.types
.Armature
.rigify_layers
= CollectionProperty(type=RigifyArmatureLayer
)
523 bpy
.types
.Armature
.active_feature_set
= EnumProperty(
524 items
=feature_set_list
.feature_set_items
,
526 description
="Restrict the rig list to a specific custom feature set"
529 bpy
.types
.PoseBone
.rigify_type
= StringProperty(name
="Rigify Type", description
="Rig type for this bone")
530 bpy
.types
.PoseBone
.rigify_parameters
= PointerProperty(type=RigifyParameters
)
532 bpy
.types
.Armature
.rigify_colors
= CollectionProperty(type=RigifyColorSet
)
534 bpy
.types
.Armature
.rigify_selection_colors
= PointerProperty(type=RigifySelectionColors
)
536 bpy
.types
.Armature
.rigify_colors_index
= IntProperty(default
=-1)
537 bpy
.types
.Armature
.rigify_colors_lock
= BoolProperty(default
=True)
538 bpy
.types
.Armature
.rigify_theme_to_add
= EnumProperty(items
=(
539 ('THEME01', 'THEME01', ''),
540 ('THEME02', 'THEME02', ''),
541 ('THEME03', 'THEME03', ''),
542 ('THEME04', 'THEME04', ''),
543 ('THEME05', 'THEME05', ''),
544 ('THEME06', 'THEME06', ''),
545 ('THEME07', 'THEME07', ''),
546 ('THEME08', 'THEME08', ''),
547 ('THEME09', 'THEME09', ''),
548 ('THEME10', 'THEME10', ''),
549 ('THEME11', 'THEME11', ''),
550 ('THEME12', 'THEME12', ''),
551 ('THEME13', 'THEME13', ''),
552 ('THEME14', 'THEME14', ''),
553 ('THEME15', 'THEME15', ''),
554 ('THEME16', 'THEME16', ''),
555 ('THEME17', 'THEME17', ''),
556 ('THEME18', 'THEME18', ''),
557 ('THEME19', 'THEME19', ''),
558 ('THEME20', 'THEME20', '')
561 id_store
= bpy
.types
.WindowManager
562 id_store
.rigify_collection
= EnumProperty(
563 items
=(("All", "All", "All"),), default
="All",
564 name
="Rigify Active Collection",
565 description
="The selected rig collection")
567 id_store
.rigify_widgets
= CollectionProperty(type=RigifyName
)
568 id_store
.rigify_types
= CollectionProperty(type=RigifyName
)
569 id_store
.rigify_active_type
= IntProperty(name
="Rigify Active Type",
570 description
="The selected rig type")
572 bpy
.types
.Armature
.rigify_force_widget_update
= BoolProperty(
573 name
="Overwrite Widget Meshes",
574 description
="Forces Rigify to delete and rebuild all of the rig widget objects. By "
575 "default, already existing widgets are reused as-is to facilitate manual "
579 bpy
.types
.Armature
.rigify_mirror_widgets
= BoolProperty(
580 name
="Mirror Widgets",
581 description
="Make widgets for left and right side bones linked duplicates with negative "
582 "X scale for the right side, based on bone name symmetry",
585 bpy
.types
.Armature
.rigify_widgets_collection
= PointerProperty(
586 type=bpy
.types
.Collection
,
587 name
="Widgets Collection",
588 description
="Defines which collection to place widget objects in. If unset, a new one "
589 "will be created based on the name of the rig")
591 bpy
.types
.Armature
.rigify_rig_basename
= StringProperty(
592 name
="Rigify Rig Name",
593 description
="Optional. If specified, this name will be used for the newly generated rig, "
594 "widget collection and script. Otherwise, a name is generated based on the "
595 "name of the metarig object by replacing 'metarig' with 'rig', 'META' with "
596 "'RIG', or prefixing with 'RIG-'. When updating an already generated rig its "
597 "name is never changed",
600 bpy
.types
.Armature
.rigify_target_rig
= PointerProperty(
601 type=bpy
.types
.Object
,
602 name
="Rigify Target Rig",
603 description
="Defines which rig to overwrite. If unset, a new one will be created with "
604 "name based on the Rig Name option or the name of the metarig",
605 poll
=lambda self
, obj
: obj
.type == 'ARMATURE' and obj
.data
is not self
)
607 bpy
.types
.Armature
.rigify_rig_ui
= PointerProperty(
609 name
="Rigify Target Rig UI",
610 description
="Defines the UI to overwrite. If unset, a new one will be created and named "
611 "based on the name of the rig")
613 bpy
.types
.Armature
.rigify_finalize_script
= PointerProperty(
615 name
="Finalize Script",
616 description
="Run this script after generation to apply user-specific changes")
618 id_store
.rigify_transfer_only_selected
= BoolProperty(
619 name
="Transfer Only Selected",
620 description
="Transfer selected bones only", default
=True)
622 prefs
= RigifyPreferences
.get_instance()
623 prefs
.register_feature_sets(True)
624 prefs
.update_external_rigs()
627 register_rig_parameters()
630 def register_rig_parameters():
631 for rig
in rig_lists
.rigs
:
632 rig_module
= rig_lists
.rigs
[rig
]['module']
633 rig_class
= rig_module
.Rig
634 rig_def
= rig_class
if hasattr(rig_class
, 'add_parameters') else rig_module
635 # noinspection PyBroadException
637 if hasattr(rig_def
, 'add_parameters'):
638 validator
= RigifyParameterValidator(RigifyParameters
, rig
, RIGIFY_PARAMETER_TABLE
)
639 rig_def
.add_parameters(validator
)
642 traceback
.print_exc()
646 from bpy
.utils
import unregister_class
648 prefs
= RigifyPreferences
.get_instance()
649 prefs
.register_feature_sets(False)
651 # Properties on PoseBones and Armature. (Annotated to suppress unknown attribute warnings.)
652 pose_bone
: typing
.Any
= bpy
.types
.PoseBone
654 del pose_bone
.rigify_type
655 del pose_bone
.rigify_parameters
657 arm_store
: typing
.Any
= bpy
.types
.Armature
659 del arm_store
.rigify_layers
660 del arm_store
.active_feature_set
661 del arm_store
.rigify_colors
662 del arm_store
.rigify_selection_colors
663 del arm_store
.rigify_colors_index
664 del arm_store
.rigify_colors_lock
665 del arm_store
.rigify_theme_to_add
666 del arm_store
.rigify_force_widget_update
667 del arm_store
.rigify_target_rig
668 del arm_store
.rigify_rig_ui
670 id_store
: typing
.Any
= bpy
.types
.WindowManager
672 del id_store
.rigify_collection
673 del id_store
.rigify_types
674 del id_store
.rigify_active_type
675 del id_store
.rigify_transfer_only_selected
679 unregister_class(cls
)
681 clear_rigify_parameters()
684 operators
.unregister()
685 metarig_menu
.unregister()
687 feature_set_list
.unregister()