Remove deprecated 2D_/3D_ prefix
[blender-addons.git] / rigify / __init__.py
blobe4cea2933dc7df1bf698bfb4146452e00958444f
1 # SPDX-License-Identifier: GPL-2.0-or-later
3 bl_info = {
4 "name": "Rigify",
5 "version": (0, 6, 7),
6 "author": "Nathan Vegdahl, Lucio Rossi, Ivan Cappiello, Alexander Gavrilov", # noqa
7 "blender": (3, 0, 0),
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",
14 import importlib
15 import sys
16 import bpy
17 import typing
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 = [
25 'utils.errors',
26 'utils.misc',
27 'utils.rig',
28 'utils.naming',
29 'utils.bones',
30 'utils.collections',
31 'utils.layers',
32 'utils.widgets',
33 'utils.widgets_basic',
34 'utils.widgets_special',
35 'utils',
36 'utils.mechanism',
37 'utils.animation',
38 'utils.metaclass',
39 'feature_sets',
40 'rigs',
41 'rigs.utils',
42 'base_rig',
43 'base_generate',
44 'feature_set_list',
45 'rig_lists',
46 'metarig_menu',
47 'rig_ui_template',
48 'utils.action_layers',
49 'generate',
50 'rot_mode',
51 'operators',
52 'ui',
56 def get_loaded_modules():
57 prefix = __name__ + '.'
58 return [name for name in sys.modules if name.startswith(prefix)]
61 def reload_modules():
62 fixed_modules = set(reload_list)
64 for name in get_loaded_modules():
65 if name not in fixed_modules:
66 del sys.modules[name]
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'
77 a_copy = list(a)
78 a_copy.remove(utils_module_name)
79 b_copy = list(b)
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)
97 return names
100 def load_rigs():
101 rig_lists.get_internal_rigs()
102 metarig_menu.init_metarig_menu()
105 if "reload_list" in locals():
106 reload_modules()
107 else:
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)
119 load_rigs()
122 from bpy.types import AddonPreferences # noqa: E402
123 from bpy.props import ( # noqa: E402
124 BoolProperty,
125 IntProperty,
126 EnumProperty,
127 StringProperty,
128 FloatVectorProperty,
129 PointerProperty,
130 CollectionProperty,
134 def get_generator():
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(
148 name="Enabled",
149 description="Whether this feature-set is registered or not",
150 update=toggle_feature_set,
151 default=True
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'}:
163 row = layout.row()
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'}:
170 pass
173 class RigifyPreferences(AddonPreferences):
174 # this must match the addon name, use '__package__'
175 # when defining this in a submodule of a python package.
176 bl_idname = __name__
178 @staticmethod
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)
182 return prefs
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:
209 break
210 else:
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()
222 # Reload rigs
223 print('Reloading external rigs...')
224 rig_lists.get_external_rigs(set_list)
226 # Reload metarigs
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')
243 row = layout.row()
244 row.template_list(
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]
257 if active_fs:
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()
269 split_factor = 0.15
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)
276 if mod:
277 split = col.row().split(factor=split_factor)
278 split.label(text="File:")
279 split.label(text=mod.__file__, translate=False)
281 if 'author' in info:
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:")
297 row = split.row()
298 if 'link' in info:
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(
318 name="object_color",
319 subtype='COLOR',
320 default=(1.0, 1.0, 1.0),
321 min=0.0, max=1.0,
322 description="color picker"
324 normal: FloatVectorProperty(
325 name="object_color",
326 subtype='COLOR',
327 default=(1.0, 1.0, 1.0),
328 min=0.0, max=1.0,
329 description="color picker"
331 select: FloatVectorProperty(
332 name="object_color",
333 subtype='COLOR',
334 default=(1.0, 1.0, 1.0),
335 min=0.0, max=1.0,
336 description="color picker"
338 standard_colors_lock: BoolProperty(default=True)
341 class RigifySelectionColors(bpy.types.PropertyGroup):
342 select: FloatVectorProperty(
343 name="object_color",
344 subtype='COLOR',
345 default=(0.314, 0.784, 1.0),
346 min=0.0, max=1.0,
347 description="color picker"
350 active: FloatVectorProperty(
351 name="object_color",
352 subtype='COLOR',
353 default=(0.549, 1.0, 1.0),
354 min=0.0, max=1.0,
355 description="color picker"
359 class RigifyParameters(bpy.types.PropertyGroup):
360 name: StringProperty()
363 # Parameter update callback
365 in_update = False
368 def update_callback(prop_name):
369 from .utils.rig import get_rigify_type
371 def callback(params, context):
372 global in_update
373 # Do not recursively call if the callback updates other parameters
374 if not in_update:
375 try:
376 in_update = True
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)
381 if rig_info:
382 rig_cb = getattr(rig_info["module"].Rig, 'on_parameter_update', None)
383 if rig_cb:
384 rig_cb(context, bone, params, prop_name)
385 finally:
386 in_update = False
388 return callback
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.
421 __params = None
422 __rig_name = ''
423 __prop_table = {}
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")
441 return
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):
467 def get_group(self):
468 if 'group_prop' in self.keys():
469 return self['group_prop']
470 else:
471 return 0
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)
478 else:
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')
492 ####################
493 # REGISTER
495 classes = (
496 RigifyName,
497 RigifyParameters,
498 RigifyColorSet,
499 RigifySelectionColors,
500 RigifyArmatureLayer,
501 RIGIFY_UL_FeatureSets,
502 RigifyFeatureSets,
503 RigifyPreferences,
507 def register():
508 from bpy.utils import register_class
510 # Sub-modules.
511 ui.register()
512 feature_set_list.register()
513 metarig_menu.register()
514 operators.register()
516 # Classes.
517 for cls in classes:
518 register_class(cls)
520 # Properties.
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,
525 name="Feature Set",
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', '')
559 ), name='Theme')
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 "
576 "editing",
577 default=False)
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",
583 default=True)
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",
598 default="")
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(
608 type=bpy.types.Text,
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(
614 type=bpy.types.Text,
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()
626 # Add rig parameters
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
636 try:
637 if hasattr(rig_def, 'add_parameters'):
638 validator = RigifyParameterValidator(RigifyParameters, rig, RIGIFY_PARAMETER_TABLE)
639 rig_def.add_parameters(validator)
640 except Exception:
641 import traceback
642 traceback.print_exc()
645 def unregister():
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
677 # Classes.
678 for cls in classes:
679 unregister_class(cls)
681 clear_rigify_parameters()
683 # Sub-modules.
684 operators.unregister()
685 metarig_menu.unregister()
686 ui.unregister()
687 feature_set_list.unregister()