Cleanup: trailing space
[blender-addons.git] / rigify / __init__.py
blob4f31459b2e6dfcfb7d7a72a4bc397cf110284581
1 # SPDX-License-Identifier: GPL-2.0-or-later
3 # <pep8 compliant>
5 bl_info = {
6 "name": "Rigify",
7 "version": (0, 6, 5),
8 "author": "Nathan Vegdahl, Lucio Rossi, Ivan Cappiello, Alexander Gavrilov",
9 "blender": (3, 0, 0),
10 "description": "Automatic rigging from building-block components",
11 "location": "Armature properties, Bone properties, View3d tools panel, Armature Add menu",
12 "doc_url": "{BLENDER_MANUAL_URL}/addons/rigging/rigify/index.html",
13 "category": "Rigging",
16 import importlib
17 import sys
18 import bpy
21 # The order in which core modules of the addon are loaded and reloaded.
22 # Modules not in this list are removed from memory upon reload.
23 # With the sole exception of 'utils', modules must be listed in the
24 # correct dependency order.
25 initial_load_order = [
26 'utils.errors',
27 'utils.misc',
28 'utils.rig',
29 'utils.naming',
30 'utils.bones',
31 'utils.collections',
32 'utils.layers',
33 'utils.widgets',
34 'utils.widgets_basic',
35 'utils.widgets_special',
36 'utils',
37 'utils.mechanism',
38 'utils.animation',
39 'utils.metaclass',
40 'feature_sets',
41 'rigs',
42 'rigs.utils',
43 'base_rig',
44 'base_generate',
45 'feature_set_list',
46 'rig_lists',
47 'metarig_menu',
48 'rig_ui_template',
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)]
60 def reload_modules():
61 fixed_modules = set(reload_list)
63 for name in get_loaded_modules():
64 if name not in fixed_modules:
65 del sys.modules[name]
67 for name in reload_list:
68 importlib.reload(sys.modules[name])
70 def compare_module_list(a, b):
71 # HACK: ignore the "utils" module when comparing module load orders,
72 # because it is inconsistent for reasons unknown.
73 # See rBAa918332cc3f821f5a70b1de53b65dd9ca596b093.
74 utils_module_name = __name__ + '.utils'
75 a_copy = list(a)
76 a_copy.remove(utils_module_name)
77 b_copy = list(b)
78 b_copy.remove(utils_module_name)
79 return a_copy == b_copy
81 def load_initial_modules():
82 load_list = [ __name__ + '.' + name for name in initial_load_order ]
84 for i, name in enumerate(load_list):
85 importlib.import_module(name)
87 module_list = get_loaded_modules()
88 expected_list = load_list[0 : max(11, i+1)]
90 if not compare_module_list(module_list, expected_list):
91 print('!!! RIGIFY: initial load order mismatch after '+name+' - expected: \n', expected_list, '\nGot:\n', module_list)
93 return load_list
95 def load_rigs():
96 rig_lists.get_internal_rigs()
97 metarig_menu.init_metarig_menu()
100 if "reload_list" in locals():
101 reload_modules()
102 else:
103 load_list = load_initial_modules()
105 from . import (base_rig, base_generate, rig_ui_template, feature_set_list, rig_lists, generate, ui, metarig_menu)
107 reload_list = reload_list_init = get_loaded_modules()
109 if not compare_module_list(reload_list, load_list):
110 print('!!! RIGIFY: initial load order mismatch - expected: \n', load_list, '\nGot:\n', reload_list)
112 load_rigs()
115 from bpy.types import AddonPreferences
116 from bpy.props import (
117 BoolProperty,
118 IntProperty,
119 EnumProperty,
120 StringProperty,
121 FloatVectorProperty,
122 PointerProperty,
123 CollectionProperty,
127 def get_generator():
128 """Returns the currently active generator instance."""
129 return base_generate.BaseGenerator.instance
132 class RigifyFeatureSets(bpy.types.PropertyGroup):
133 name: bpy.props.StringProperty()
134 module_name: bpy.props.StringProperty()
136 def toggle_featureset(self, context):
137 feature_set_list.call_register_function(self.module_name, self.enabled)
138 context.preferences.addons[__package__].preferences.update_external_rigs()
140 enabled: bpy.props.BoolProperty(
141 name = "Enabled",
142 description = "Whether this feature-set is registered or not",
143 update = toggle_featureset,
144 default = True
148 class RIGIFY_UL_FeatureSets(bpy.types.UIList):
149 def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
150 rigify_prefs = data
151 feature_sets = rigify_prefs.rigify_feature_sets
152 active_set = feature_sets[rigify_prefs.active_feature_set_index]
153 feature_set_entry = item
154 if self.layout_type in {'DEFAULT', 'COMPACT'}:
155 row = layout.row()
156 row.prop(feature_set_entry, 'name', text="", emboss=False)
158 icon = 'CHECKBOX_HLT' if feature_set_entry.enabled else 'CHECKBOX_DEHLT'
159 row.enabled = feature_set_entry.enabled
160 layout.prop(feature_set_entry, 'enabled', text="", icon=icon, emboss=False)
161 elif self.layout_type in {'GRID'}:
162 pass
164 class RigifyPreferences(AddonPreferences):
165 # this must match the addon name, use '__package__'
166 # when defining this in a submodule of a python package.
167 bl_idname = __name__
169 def register_feature_sets(self, register):
170 """Call register or unregister of external feature sets"""
171 for set_name in feature_set_list.get_enabled_modules_names():
172 feature_set_list.call_register_function(set_name, register)
174 def refresh_installed_feature_sets(self):
175 """Synchronize preferences entries with what's actually in the file system."""
176 feature_set_prefs = self.rigify_feature_sets
178 module_names = feature_set_list.get_installed_modules_names()
180 # If there is a feature set preferences entry with no corresponding
181 # installed module, user must've manually removed it from the filesystem,
182 # so let's remove such entries.
183 to_delete = [ i for i, fs in enumerate(feature_set_prefs) if fs.module_name not in module_names ]
184 for i in reversed(to_delete):
185 feature_set_prefs.remove(i)
187 # If there is an installed feature set in the file system but no corresponding
188 # entry, user must've installed it manually. Make sure it has an entry.
189 for module_name in module_names:
190 for fs in feature_set_prefs:
191 if module_name == fs.module_name:
192 break
193 else:
194 fs = feature_set_prefs.add()
195 fs.name = feature_set_list.get_ui_name(module_name)
196 fs.module_name = module_name
198 def update_external_rigs(self):
199 """Get external feature sets"""
201 self.refresh_installed_feature_sets()
203 set_list = feature_set_list.get_enabled_modules_names()
205 # Reload rigs
206 print('Reloading external rigs...')
207 rig_lists.get_external_rigs(set_list)
209 # Reload metarigs
210 print('Reloading external metarigs...')
211 metarig_menu.get_external_metarigs(set_list)
213 # Re-register rig parameters
214 register_rig_parameters()
216 rigify_feature_sets: bpy.props.CollectionProperty(type=RigifyFeatureSets)
217 active_feature_set_index: IntProperty()
219 def draw(self, context):
220 layout = self.layout
222 layout.label(text="Feature Sets:")
224 layout.operator("wm.rigify_add_feature_set", text="Install Feature Set from File...", icon='FILEBROWSER')
226 row = layout.row()
227 row.template_list(
228 'RIGIFY_UL_FeatureSets',
230 self, "rigify_feature_sets",
231 self, 'active_feature_set_index'
234 # Clamp active index to ensure it's in bounds.
235 self.active_feature_set_index = max(0, min(self.active_feature_set_index, len(self.rigify_feature_sets)-1))
237 if len(self.rigify_feature_sets) > 0:
238 active_fs = self.rigify_feature_sets[self.active_feature_set_index]
240 if active_fs:
241 draw_feature_set_prefs(layout, context, active_fs)
244 def draw_feature_set_prefs(layout, context, featureset: RigifyFeatureSets):
245 info = feature_set_list.get_info_dict(featureset.module_name)
247 description = featureset.name
248 if 'description' in info:
249 description = info['description']
251 col = layout.column()
252 split_factor = 0.15
254 split = col.row().split(factor=split_factor)
255 split.label(text="Description:")
256 split.label(text=description)
258 mod = feature_set_list.get_module_safe(featureset.module_name)
259 if mod:
260 split = col.row().split(factor=split_factor)
261 split.label(text="File:")
262 split.label(text=mod.__file__, translate=False)
264 if 'author' in info:
265 split = col.row().split(factor=split_factor)
266 split.label(text="Author:")
267 split.label(text=info["author"])
269 if 'version' in info:
270 split = col.row().split(factor=split_factor)
271 split.label(text="Version:")
272 split.label(text=".".join(str(x) for x in info['version']), translate=False)
273 if 'warning' in info:
274 split = col.row().split(factor=split_factor)
275 split.label(text="Warning:")
276 split.label(text=" " + info['warning'], icon='ERROR')
278 split = col.row().split(factor=split_factor)
279 split.label(text="Internet:")
280 row = split.row()
281 if 'link' in info:
282 op = row.operator('wm.url_open', text="Repository", icon='URL')
283 op.url = info['link']
284 if 'doc_url' in info:
285 op = row.operator('wm.url_open', text="Documentation", icon='HELP')
286 op.url = info['doc_url']
287 if 'tracker_url' in info:
288 op = row.operator('wm.url_open', text="Report a Bug", icon='URL')
289 op.url = info['tracker_url']
291 row.operator("wm.rigify_remove_feature_set", text="Remove", icon='CANCEL')
294 class RigifyName(bpy.types.PropertyGroup):
295 name: StringProperty()
298 class RigifyColorSet(bpy.types.PropertyGroup):
299 name: StringProperty(name="Color Set", default=" ")
300 active: FloatVectorProperty(
301 name="object_color",
302 subtype='COLOR',
303 default=(1.0, 1.0, 1.0),
304 min=0.0, max=1.0,
305 description="color picker"
307 normal: FloatVectorProperty(
308 name="object_color",
309 subtype='COLOR',
310 default=(1.0, 1.0, 1.0),
311 min=0.0, max=1.0,
312 description="color picker"
314 select: FloatVectorProperty(
315 name="object_color",
316 subtype='COLOR',
317 default=(1.0, 1.0, 1.0),
318 min=0.0, max=1.0,
319 description="color picker"
321 standard_colors_lock: BoolProperty(default=True)
324 class RigifySelectionColors(bpy.types.PropertyGroup):
326 select: FloatVectorProperty(
327 name="object_color",
328 subtype='COLOR',
329 default=(0.314, 0.784, 1.0),
330 min=0.0, max=1.0,
331 description="color picker"
334 active: FloatVectorProperty(
335 name="object_color",
336 subtype='COLOR',
337 default=(0.549, 1.0, 1.0),
338 min=0.0, max=1.0,
339 description="color picker"
343 class RigifyParameters(bpy.types.PropertyGroup):
344 name: StringProperty()
346 # Parameter update callback
348 in_update = False
350 def update_callback(prop_name):
351 from .utils.rig import get_rigify_type
353 def callback(params, context):
354 global in_update
355 # Do not recursively call if the callback updates other parameters
356 if not in_update:
357 try:
358 in_update = True
359 bone = context.active_pose_bone
361 if bone and bone.rigify_parameters == params:
362 rig_info = rig_lists.rigs.get(get_rigify_type(bone), None)
363 if rig_info:
364 rig_cb = getattr(rig_info["module"].Rig, 'on_parameter_update', None)
365 if rig_cb:
366 rig_cb(context, bone, params, prop_name)
367 finally:
368 in_update = False
370 return callback
372 # Remember the initial property set
373 RIGIFY_PARAMETERS_BASE_DIR = set(dir(RigifyParameters))
375 RIGIFY_PARAMETER_TABLE = {'name': ('DEFAULT', StringProperty())}
377 def clear_rigify_parameters():
378 for name in list(dir(RigifyParameters)):
379 if name not in RIGIFY_PARAMETERS_BASE_DIR:
380 delattr(RigifyParameters, name)
381 if name in RIGIFY_PARAMETER_TABLE:
382 del RIGIFY_PARAMETER_TABLE[name]
385 def format_property_spec(spec):
386 """Turns the return value of bpy.props.SomeProperty(...) into a readable string."""
387 callback, params = spec
388 param_str = ["%s=%r" % (k, v) for k, v in params.items()]
389 return "%s(%s)" % (callback.__name__, ', '.join(param_str))
392 class RigifyParameterValidator(object):
394 A wrapper around RigifyParameters that verifies properties
395 defined from rigs for incompatible redefinitions using a table.
397 Relies on the implementation details of bpy.props return values:
398 specifically, they just return a tuple containing the real define
399 function, and a dictionary with parameters. This allows comparing
400 parameters before the property is actually defined.
402 __params = None
403 __rig_name = ''
404 __prop_table = {}
406 def __init__(self, params, rig_name, prop_table):
407 self.__params = params
408 self.__rig_name = rig_name
409 self.__prop_table = prop_table
411 def __getattr__(self, name):
412 return getattr(self.__params, name)
414 def __setattr__(self, name, val_original):
415 # allow __init__ to work correctly
416 if hasattr(RigifyParameterValidator, name):
417 return object.__setattr__(self, name, val_original)
419 if not isinstance(val_original, bpy.props._PropertyDeferred):
420 print("!!! RIGIFY RIG %s: INVALID DEFINITION FOR RIG PARAMETER %s: %r\n" % (self.__rig_name, name, val_original))
421 return
423 # actually defining the property modifies the dictionary with new parameters, so copy it now
424 val = (val_original.function, val_original.keywords)
425 new_def = (val[0], val[1].copy())
427 if 'poll' in new_def[1]:
428 del new_def[1]['poll']
430 if name in self.__prop_table:
431 cur_rig, cur_info = self.__prop_table[name]
432 if new_def != cur_info:
433 print("!!! RIGIFY RIG %s: REDEFINING PARAMETER %s AS:\n\n %s\n" % (self.__rig_name, name, format_property_spec(val)))
434 print("!!! PREVIOUS DEFINITION BY %s:\n\n %s\n" % (cur_rig, format_property_spec(cur_info)))
436 # inject a generic update callback that calls the appropriate rig classmethod
437 if val[0] != bpy.props.CollectionProperty:
438 val[1]['update'] = update_callback(name)
440 setattr(self.__params, name, val_original)
441 self.__prop_table[name] = (self.__rig_name, new_def)
444 class RigifyArmatureLayer(bpy.types.PropertyGroup):
446 def get_group(self):
447 if 'group_prop' in self.keys():
448 return self['group_prop']
449 else:
450 return 0
452 def set_group(self, value):
453 arm = bpy.context.object.data
454 if value > len(arm.rigify_colors):
455 self['group_prop'] = len(arm.rigify_colors)
456 else:
457 self['group_prop'] = value
459 name: StringProperty(name="Layer Name", default=" ")
460 row: IntProperty(name="Layer Row", default=1, min=1, max=32, description='UI row for this layer')
461 selset: BoolProperty(name="Selection Set", default=False, description='Add Selection Set for this layer')
462 group: IntProperty(name="Bone Group", default=0, min=0, max=32,
463 get=get_group, set=set_group, description='Assign Bone Group to this layer')
466 ##### REGISTER #####
468 classes = (
469 RigifyName,
470 RigifyParameters,
471 RigifyColorSet,
472 RigifySelectionColors,
473 RigifyArmatureLayer,
474 RIGIFY_UL_FeatureSets,
475 RigifyFeatureSets,
476 RigifyPreferences,
480 def register():
481 from bpy.utils import register_class
483 # Sub-modules.
484 ui.register()
485 feature_set_list.register()
486 metarig_menu.register()
487 operators.register()
489 # Classes.
490 for cls in classes:
491 register_class(cls)
493 # Properties.
494 bpy.types.Armature.rigify_layers = CollectionProperty(type=RigifyArmatureLayer)
496 bpy.types.Armature.active_feature_set = EnumProperty(
497 items=feature_set_list.feature_set_items,
498 name="Feature Set",
499 description="Restrict the rig list to a specific custom feature set"
502 bpy.types.PoseBone.rigify_type = StringProperty(name="Rigify Type", description="Rig type for this bone")
503 bpy.types.PoseBone.rigify_parameters = PointerProperty(type=RigifyParameters)
505 bpy.types.Armature.rigify_colors = CollectionProperty(type=RigifyColorSet)
507 bpy.types.Armature.rigify_selection_colors = PointerProperty(type=RigifySelectionColors)
509 bpy.types.Armature.rigify_colors_index = IntProperty(default=-1)
510 bpy.types.Armature.rigify_colors_lock = BoolProperty(default=True)
511 bpy.types.Armature.rigify_theme_to_add = EnumProperty(items=(
512 ('THEME01', 'THEME01', ''),
513 ('THEME02', 'THEME02', ''),
514 ('THEME03', 'THEME03', ''),
515 ('THEME04', 'THEME04', ''),
516 ('THEME05', 'THEME05', ''),
517 ('THEME06', 'THEME06', ''),
518 ('THEME07', 'THEME07', ''),
519 ('THEME08', 'THEME08', ''),
520 ('THEME09', 'THEME09', ''),
521 ('THEME10', 'THEME10', ''),
522 ('THEME11', 'THEME11', ''),
523 ('THEME12', 'THEME12', ''),
524 ('THEME13', 'THEME13', ''),
525 ('THEME14', 'THEME14', ''),
526 ('THEME15', 'THEME15', ''),
527 ('THEME16', 'THEME16', ''),
528 ('THEME17', 'THEME17', ''),
529 ('THEME18', 'THEME18', ''),
530 ('THEME19', 'THEME19', ''),
531 ('THEME20', 'THEME20', '')
532 ), name='Theme')
534 IDStore = bpy.types.WindowManager
535 IDStore.rigify_collection = EnumProperty(items=(("All", "All", "All"),), default="All",
536 name="Rigify Active Collection",
537 description="The selected rig collection")
539 IDStore.rigify_widgets = CollectionProperty(type=RigifyName)
541 IDStore.rigify_types = CollectionProperty(type=RigifyName)
542 IDStore.rigify_active_type = IntProperty(name="Rigify Active Type", description="The selected rig type")
544 bpy.types.Armature.rigify_force_widget_update = BoolProperty(name="Force Widget Update",
545 description="Forces Rigify to delete and rebuild all the rig widgets. if unset, only missing widgets will be created",
546 default=False)
548 bpy.types.Armature.rigify_mirror_widgets = BoolProperty(name="Mirror Widgets",
549 description="Make widgets for left and right side bones linked duplicates with negative X scale for the right side, based on bone name symmetry",
550 default=True)
551 bpy.types.Armature.rigify_widgets_collection = PointerProperty(type=bpy.types.Collection,
552 name="Widgets Collection",
553 description="Defines which collection to place widget objects in. If unset, a new one will be created based on the name of the rig")
555 bpy.types.Armature.rigify_target_rig = PointerProperty(type=bpy.types.Object,
556 name="Rigify Target Rig",
557 description="Defines which rig to overwrite. If unset, a new one called 'rig' will be created",
558 poll=lambda self, obj: obj.type == 'ARMATURE' and obj.data is not self)
560 bpy.types.Armature.rigify_rig_ui = PointerProperty(type=bpy.types.Text,
561 name="Rigify Target Rig UI",
562 description="Defines the UI to overwrite. If unset, 'rig_ui.py' will be used")
564 bpy.types.Armature.rigify_finalize_script = PointerProperty(type=bpy.types.Text,
565 name="Finalize Script",
566 description="Run this script after generation to apply user-specific changes")
567 IDStore.rigify_transfer_only_selected = BoolProperty(
568 name="Transfer Only Selected",
569 description="Transfer selected bones only", default=True)
571 bpy.context.preferences.addons[__package__].preferences.register_feature_sets(True)
572 bpy.context.preferences.addons[__package__].preferences.update_external_rigs()
574 # Add rig parameters
575 register_rig_parameters()
578 def register_rig_parameters():
579 for rig in rig_lists.rigs:
580 rig_module = rig_lists.rigs[rig]['module']
581 rig_class = rig_module.Rig
582 r = rig_class if hasattr(rig_class, 'add_parameters') else rig_module
583 try:
584 if hasattr(r, 'add_parameters'):
585 r.add_parameters(RigifyParameterValidator(RigifyParameters, rig, RIGIFY_PARAMETER_TABLE))
586 except Exception:
587 import traceback
588 traceback.print_exc()
591 def unregister():
592 from bpy.utils import unregister_class
594 bpy.context.preferences.addons[__package__].preferences.register_feature_sets(False)
596 # Properties on PoseBones and Armature.
597 del bpy.types.PoseBone.rigify_type
598 del bpy.types.PoseBone.rigify_parameters
600 ArmStore = bpy.types.Armature
601 del ArmStore.rigify_layers
602 del ArmStore.active_feature_set
603 del ArmStore.rigify_colors
604 del ArmStore.rigify_selection_colors
605 del ArmStore.rigify_colors_index
606 del ArmStore.rigify_colors_lock
607 del ArmStore.rigify_theme_to_add
608 del ArmStore.rigify_force_widget_update
609 del ArmStore.rigify_target_rig
610 del ArmStore.rigify_rig_ui
612 IDStore = bpy.types.WindowManager
613 del IDStore.rigify_collection
614 del IDStore.rigify_types
615 del IDStore.rigify_active_type
616 del IDStore.rigify_transfer_only_selected
618 # Classes.
619 for cls in classes:
620 unregister_class(cls)
622 clear_rigify_parameters()
624 # Sub-modules.
625 operators.unregister()
626 metarig_menu.unregister()
627 ui.unregister()
628 feature_set_list.unregister()