1#====================== BEGIN GPL LICENSE BLOCK ======================
2#
3#  This program is free software; you can redistribute it and/or
4#  modify it under the terms of the GNU General Public License
5#  as published by the Free Software Foundation; either version 2
6#  of the License, or (at your option) any later version.
7#
8#  This program is distributed in the hope that it will be useful,
9#  but WITHOUT ANY WARRANTY; without even the implied warranty of
10#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11#  GNU General Public License for more details.
12#
13#  You should have received a copy of the GNU General Public License
14#  along with this program; if not, write to the Free Software Foundation,
15#  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16#
17#======================= END GPL LICENSE BLOCK ========================
18
19# <pep8 compliant>
20
21bl_info = {
22    "name": "Rigify",
23    "version": (0, 6, 1),
24    "author": "Nathan Vegdahl, Lucio Rossi, Ivan Cappiello, Alexander Gavrilov",
25    "blender": (2, 81, 0),
26    "description": "Automatic rigging from building-block components",
27    "location": "Armature properties, Bone properties, View3d tools panel, Armature Add menu",
28    "doc_url": "{BLENDER_MANUAL_URL}/addons/rigging/rigify/index.html",
29    "category": "Rigging",
30}
31
32import importlib
33import sys
34import bpy
35
36
37# The order in which core modules of the addon are loaded and reloaded.
38# Modules not in this list are removed from memory upon reload.
39# With the sole exception of 'utils', modules must be listed in the
40# correct dependency order.
41initial_load_order = [
42    'utils.errors',
43    'utils.misc',
44    'utils.rig',
45    'utils.naming',
46    'utils.bones',
47    'utils.collections',
48    'utils.layers',
49    'utils.widgets',
50    'utils.widgets_basic',
51    'utils.widgets_special',
52    'utils',
53    'utils.mechanism',
54    'utils.animation',
55    'utils.metaclass',
56    'feature_sets',
57    'rigs',
58    'rigs.utils',
59    'base_rig',
60    'base_generate',
61    'feature_set_list',
62    'rig_lists',
63    'metarig_menu',
64    'rig_ui_template',
65    'generate',
66    'rot_mode',
67    'ui',
68]
69utils_module_name = __name__ + '.utils'
70
71
72def get_loaded_modules():
73    prefix = __name__ + '.'
74    return [name for name in sys.modules if name.startswith(prefix)]
75
76def reload_modules():
77    fixed_modules = set(reload_list)
78
79    for name in get_loaded_modules():
80        if name not in fixed_modules:
81            del sys.modules[name]
82
83    for name in reload_list:
84        importlib.reload(sys.modules[name])
85
86def compare_module_list(a, b):
87    # Allow 'utils' to move around
88    a_copy = list(a)
89    a_copy.remove(utils_module_name)
90    b_copy = list(b)
91    b_copy.remove(utils_module_name)
92    return a_copy == b_copy
93
94def load_initial_modules():
95    load_list = [ __name__ + '.' + name for name in initial_load_order ]
96
97    for i, name in enumerate(load_list):
98        importlib.import_module(name)
99
100        module_list = get_loaded_modules()
101        expected_list = load_list[0 : max(11, i+1)]
102
103        if not compare_module_list(module_list, expected_list):
104            print('!!! RIGIFY: initial load order mismatch after '+name+' - expected: \n', expected_list, '\nGot:\n', module_list)
105
106    return load_list
107
108def load_rigs():
109    if not legacy_loaded:
110        rig_lists.get_internal_rigs()
111        metarig_menu.init_metarig_menu()
112
113
114if "reload_list" in locals():
115    reload_modules()
116else:
117    legacy_loaded = False
118
119    load_list = load_initial_modules()
120
121    from . import (base_rig, base_generate, rig_ui_template, feature_set_list, rig_lists, generate, ui, metarig_menu)
122
123    reload_list = reload_list_init = get_loaded_modules()
124
125    if not compare_module_list(reload_list, load_list):
126        print('!!! RIGIFY: initial load order mismatch - expected: \n', load_list, '\nGot:\n', reload_list)
127
128load_rigs()
129
130
131from bpy.types import AddonPreferences
132from bpy.props import (
133    BoolProperty,
134    IntProperty,
135    EnumProperty,
136    StringProperty,
137    FloatVectorProperty,
138    PointerProperty,
139    CollectionProperty,
140)
141
142
143class RigifyPreferences(AddonPreferences):
144    # this must match the addon name, use '__package__'
145    # when defining this in a submodule of a python package.
146    bl_idname = __name__
147
148    def update_legacy(self, context):
149        global legacy_loaded, reload_list
150
151        if self.legacy_mode:
152            if legacy_loaded:    # already in legacy mode. needed when rigify is reloaded
153                return
154            else:
155                unregister()
156                reload_modules()
157
158                globals().pop('utils')
159                globals().pop('rig_lists')
160                globals().pop('generate')
161                globals().pop('ui')
162                globals().pop('metarig_menu')
163
164                from .legacy import utils, rig_lists, generate, ui, metarig_menu
165
166                print("ENTERING RIGIFY LEGACY\r\n")
167
168                legacy_loaded = True
169                reload_list += [ m.__name__ for m in [ legacy, utils, rig_lists, generate, ui, metarig_menu ] ]
170
171                globals()['utils'] = legacy.utils
172                globals()['rig_lists'] = legacy.rig_lists
173                globals()['generate'] = legacy.generate
174                globals()['ui'] = legacy.ui
175                globals()['metarig_menu'] = legacy.metarig_menu
176
177                register()
178
179        else:
180            unregister()
181
182            globals().pop('utils')
183            globals().pop('rig_lists')
184            globals().pop('generate')
185            globals().pop('ui')
186            globals().pop('metarig_menu')
187
188            from . import utils, rig_lists, generate, ui, metarig_menu
189
190            print("EXIT RIGIFY LEGACY\r\n")
191
192            globals()['utils'] = utils
193            globals()['rig_lists'] = rig_lists
194            globals()['generate'] = generate
195            globals()['ui'] = ui
196            globals()['metarig_menu'] = metarig_menu
197
198            legacy_loaded = False
199            reload_list = reload_list_init
200            reload_modules()
201
202            load_rigs()
203
204            register()
205
206    def register_feature_sets(self, register):
207        """Call register or unregister of external feature sets"""
208        if self.legacy_mode:
209            return
210
211        for set_name in feature_set_list.get_installed_list():
212            feature_set_list.call_register_function(set_name, register)
213
214    def update_external_rigs(self, force=False):
215        """Get external feature sets"""
216        if self.legacy_mode:
217            return
218
219        set_list = feature_set_list.get_installed_list()
220
221        if force or len(set_list) > 0:
222            # Reload rigs
223            print('Reloading external rigs...')
224            rig_lists.get_external_rigs(set_list)
225
226            # Reload metarigs
227            print('Reloading external metarigs...')
228            metarig_menu.get_external_metarigs(set_list)
229
230            # Re-register rig parameters
231            register_rig_parameters()
232
233    legacy_mode: BoolProperty(
234        name='Rigify Legacy Mode',
235        description='Select if you want to use Rigify in legacy mode',
236        default=False,
237        update=update_legacy
238    )
239
240    show_expanded: BoolProperty()
241
242    show_rigs_folder_expanded: BoolProperty()
243
244    def draw(self, context):
245        layout = self.layout
246        column = layout.column()
247        box = column.box()
248
249        # first stage
250        expand = getattr(self, 'show_expanded')
251        icon = 'TRIA_DOWN' if expand else 'TRIA_RIGHT'
252        col = box.column()
253        row = col.row()
254        sub = row.row()
255        sub.context_pointer_set('addon_prefs', self)
256        sub.alignment = 'LEFT'
257        op = sub.operator('wm.context_toggle', text='', icon=icon,
258                          emboss=False)
259        op.data_path = 'addon_prefs.show_expanded'
260        sub.label(text='{}: {}'.format('Rigify', 'Enable Legacy Mode'))
261        sub = row.row()
262        sub.alignment = 'RIGHT'
263        sub.prop(self, 'legacy_mode')
264
265        if expand:
266            split = col.row().split(factor=0.15)
267            split.label(text='Description:')
268            split.label(text='When enabled the add-on will run in legacy mode using the old 2.76b feature set.')
269
270        box = column.box()
271        rigs_expand = getattr(self, 'show_rigs_folder_expanded')
272        icon = 'TRIA_DOWN' if rigs_expand else 'TRIA_RIGHT'
273        col = box.column()
274        row = col.row()
275        sub = row.row()
276        sub.context_pointer_set('addon_prefs', self)
277        sub.alignment = 'LEFT'
278        op = sub.operator('wm.context_toggle', text='', icon=icon,
279                          emboss=False)
280        op.data_path = 'addon_prefs.show_rigs_folder_expanded'
281        sub.label(text='{}: {}'.format('Rigify', 'External feature sets'))
282        if rigs_expand:
283            for fs in feature_set_list.get_installed_list():
284                row = col.split(factor=0.8)
285                row.label(text=feature_set_list.get_ui_name(fs))
286                op = row.operator("wm.rigify_remove_feature_set", text="Remove", icon='CANCEL')
287                op.featureset = fs
288            row = col.row(align=True)
289            row.operator("wm.rigify_add_feature_set", text="Install Feature Set from File...", icon='FILEBROWSER')
290
291            split = col.row().split(factor=0.15)
292            split.label(text='Description:')
293            split.label(text='External feature sets (rigs, metarigs, ui layouts)')
294
295        row = layout.row()
296        row.label(text="End of Rigify Preferences")
297
298
299class RigifyName(bpy.types.PropertyGroup):
300    name: StringProperty()
301
302
303class RigifyColorSet(bpy.types.PropertyGroup):
304    name: StringProperty(name="Color Set", default=" ")
305    active: FloatVectorProperty(
306        name="object_color",
307        subtype='COLOR',
308        default=(1.0, 1.0, 1.0),
309        min=0.0, max=1.0,
310        description="color picker"
311    )
312    normal: FloatVectorProperty(
313        name="object_color",
314        subtype='COLOR',
315        default=(1.0, 1.0, 1.0),
316        min=0.0, max=1.0,
317        description="color picker"
318    )
319    select: FloatVectorProperty(
320        name="object_color",
321        subtype='COLOR',
322        default=(1.0, 1.0, 1.0),
323        min=0.0, max=1.0,
324        description="color picker"
325    )
326    standard_colors_lock: BoolProperty(default=True)
327
328
329class RigifySelectionColors(bpy.types.PropertyGroup):
330
331    select: FloatVectorProperty(
332        name="object_color",
333        subtype='COLOR',
334        default=(0.314, 0.784, 1.0),
335        min=0.0, max=1.0,
336        description="color picker"
337    )
338
339    active: FloatVectorProperty(
340        name="object_color",
341        subtype='COLOR',
342        default=(0.549, 1.0, 1.0),
343        min=0.0, max=1.0,
344        description="color picker"
345    )
346
347
348class RigifyParameters(bpy.types.PropertyGroup):
349    name: StringProperty()
350
351# Parameter update callback
352
353in_update = False
354
355def update_callback(prop_name):
356    from .utils.rig import get_rigify_type
357
358    def callback(params, context):
359        global in_update
360        # Do not recursively call if the callback updates other parameters
361        if not in_update:
362            try:
363                in_update = True
364                bone = context.active_pose_bone
365
366                if bone and bone.rigify_parameters == params:
367                    rig_info = rig_lists.rigs.get(get_rigify_type(bone), None)
368                    if rig_info:
369                        rig_cb = getattr(rig_info["module"].Rig, 'on_parameter_update', None)
370                        if rig_cb:
371                            rig_cb(context, bone, params, prop_name)
372            finally:
373                in_update = False
374
375    return callback
376
377# Remember the initial property set
378RIGIFY_PARAMETERS_BASE_DIR = set(dir(RigifyParameters))
379
380RIGIFY_PARAMETER_TABLE = {'name': ('DEFAULT', StringProperty())}
381
382def clear_rigify_parameters():
383    for name in list(dir(RigifyParameters)):
384        if name not in RIGIFY_PARAMETERS_BASE_DIR:
385            delattr(RigifyParameters, name)
386            if name in RIGIFY_PARAMETER_TABLE:
387                del RIGIFY_PARAMETER_TABLE[name]
388
389
390def format_property_spec(spec):
391    """Turns the return value of bpy.props.SomeProperty(...) into a readable string."""
392    callback, params = spec
393    param_str = ["%s=%r" % (k, v) for k, v in params.items()]
394    return "%s(%s)" % (callback.__name__, ', '.join(param_str))
395
396
397class RigifyParameterValidator(object):
398    """
399    A wrapper around RigifyParameters that verifies properties
400    defined from rigs for incompatible redefinitions using a table.
401
402    Relies on the implementation details of bpy.props return values:
403    specifically, they just return a tuple containing the real define
404    function, and a dictionary with parameters. This allows comparing
405    parameters before the property is actually defined.
406    """
407    __params = None
408    __rig_name = ''
409    __prop_table = {}
410
411    def __init__(self, params, rig_name, prop_table):
412        self.__params = params
413        self.__rig_name = rig_name
414        self.__prop_table = prop_table
415
416    def __getattr__(self, name):
417        return getattr(self.__params, name)
418
419    def __setattr__(self, name, val):
420        # allow __init__ to work correctly
421        if hasattr(RigifyParameterValidator, name):
422            return object.__setattr__(self, name, val)
423
424        if not (isinstance(val, tuple) and callable(val[0]) and isinstance(val[1], dict)):
425            print("!!! RIGIFY RIG %s: INVALID DEFINITION FOR RIG PARAMETER %s: %r\n" % (self.__rig_name, name, val))
426            return
427
428        # actually defining the property modifies the dictionary with new parameters, so copy it now
429        new_def = (val[0], val[1].copy())
430
431        if 'poll' in new_def[1]:
432            del new_def[1]['poll']
433
434        if name in self.__prop_table:
435            cur_rig, cur_info = self.__prop_table[name]
436            if new_def != cur_info:
437                print("!!! RIGIFY RIG %s: REDEFINING PARAMETER %s AS:\n\n    %s\n" % (self.__rig_name, name, format_property_spec(val)))
438                print("!!! PREVIOUS DEFINITION BY %s:\n\n    %s\n" % (cur_rig, format_property_spec(cur_info)))
439
440        # inject a generic update callback that calls the appropriate rig classmethod
441        val[1]['update'] = update_callback(name)
442
443        setattr(self.__params, name, val)
444        self.__prop_table[name] = (self.__rig_name, new_def)
445
446
447class RigifyArmatureLayer(bpy.types.PropertyGroup):
448
449    def get_group(self):
450        if 'group_prop' in self.keys():
451            return self['group_prop']
452        else:
453            return 0
454
455    def set_group(self, value):
456        arm = bpy.context.object.data
457        if value > len(arm.rigify_colors):
458            self['group_prop'] = len(arm.rigify_colors)
459        else:
460            self['group_prop'] = value
461
462    name: StringProperty(name="Layer Name", default=" ")
463    row: IntProperty(name="Layer Row", default=1, min=1, max=32, description='UI row for this layer')
464    selset: BoolProperty(name="Selection Set", default=False, description='Add Selection Set for this layer')
465    group: IntProperty(name="Bone Group", default=0, min=0, max=32,
466        get=get_group, set=set_group, description='Assign Bone Group to this layer')
467
468
469##### REGISTER #####
470
471classes = (
472    RigifyName,
473    RigifyParameters,
474    RigifyColorSet,
475    RigifySelectionColors,
476    RigifyArmatureLayer,
477    RigifyPreferences,
478)
479
480
481def register():
482    from bpy.utils import register_class
483
484    # Sub-modules.
485    ui.register()
486    feature_set_list.register()
487    metarig_menu.register()
488
489    # Classes.
490    for cls in classes:
491        register_class(cls)
492
493    # Properties.
494    bpy.types.Armature.rigify_layers = CollectionProperty(type=RigifyArmatureLayer)
495
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"
500        )
501
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)
504
505    bpy.types.Armature.rigify_colors = CollectionProperty(type=RigifyColorSet)
506
507    bpy.types.Armature.rigify_selection_colors = PointerProperty(type=RigifySelectionColors)
508
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')
533
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")
538
539    IDStore.rigify_types = CollectionProperty(type=RigifyName)
540    IDStore.rigify_active_type = IntProperty(name="Rigify Active Type", description="The selected rig type")
541
542    bpy.types.Armature.rigify_advanced_generation = BoolProperty(name="Advanced Options",
543        description="Enables/disables advanced options for Rigify rig generation",
544        default=False)
545
546    def update_mode(self, context):
547        if self.rigify_generate_mode == 'new':
548            self.rigify_force_widget_update = False
549
550    bpy.types.Armature.rigify_generate_mode = EnumProperty(name="Rigify Generate Rig Mode",
551        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",
552        update=update_mode,
553        items=( ('overwrite', 'overwrite', ''),
554                ('new', 'new', '')))
555
556    bpy.types.Armature.rigify_force_widget_update = BoolProperty(name="Force Widget Update",
557        description="Forces Rigify to delete and rebuild all the rig widgets. if unset, only missing widgets will be created",
558        default=False)
559
560    bpy.types.Armature.rigify_target_rig = PointerProperty(type=bpy.types.Object,
561        name="Rigify Target Rig",
562        description="Defines which rig to overwrite. If unset, a new one called 'rig' will be created",
563        poll=lambda self, obj: obj.type == 'ARMATURE' and obj.data is not self)
564
565    bpy.types.Armature.rigify_rig_ui = PointerProperty(type=bpy.types.Text,
566        name="Rigify Target Rig UI",
567        description="Defines the UI to overwrite. If unset, 'rig_ui.py' will be used")
568
569    bpy.types.Armature.rigify_rig_basename = StringProperty(name="Rigify Rig Name",
570        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",
571        default="")
572
573    IDStore.rigify_transfer_only_selected = BoolProperty(
574        name="Transfer Only Selected",
575        description="Transfer selected bones only", default=True)
576
577    # Update legacy on restart or reload.
578    if legacy_loaded or bpy.context.preferences.addons['rigify'].preferences.legacy_mode:
579        bpy.context.preferences.addons['rigify'].preferences.legacy_mode = True
580
581    bpy.context.preferences.addons['rigify'].preferences.register_feature_sets(True)
582    bpy.context.preferences.addons['rigify'].preferences.update_external_rigs()
583
584    # Add rig parameters
585    register_rig_parameters()
586
587
588def register_rig_parameters():
589    if bpy.context.preferences.addons['rigify'].preferences.legacy_mode:
590        for rig in rig_lists.rig_list:
591            r = utils.get_rig_type(rig)
592            try:
593                r.add_parameters(RigifyParameterValidator(RigifyParameters, rig, RIGIFY_PARAMETER_TABLE))
594            except AttributeError:
595                pass
596    else:
597        for rig in rig_lists.rigs:
598            rig_module = rig_lists.rigs[rig]['module']
599            rig_class = rig_module.Rig
600            r = rig_class if hasattr(rig_class, 'add_parameters') else rig_module
601            try:
602                if hasattr(r, 'add_parameters'):
603                    r.add_parameters(RigifyParameterValidator(RigifyParameters, rig, RIGIFY_PARAMETER_TABLE))
604            except Exception:
605                import traceback
606                traceback.print_exc()
607
608
609def unregister():
610    from bpy.utils import unregister_class
611
612    bpy.context.preferences.addons['rigify'].preferences.register_feature_sets(False)
613
614    # Properties on PoseBones and Armature.
615    del bpy.types.PoseBone.rigify_type
616    del bpy.types.PoseBone.rigify_parameters
617
618    ArmStore = bpy.types.Armature
619    del ArmStore.rigify_layers
620    del ArmStore.active_feature_set
621    del ArmStore.rigify_colors
622    del ArmStore.rigify_selection_colors
623    del ArmStore.rigify_colors_index
624    del ArmStore.rigify_colors_lock
625    del ArmStore.rigify_theme_to_add
626    del ArmStore.rigify_advanced_generation
627    del ArmStore.rigify_generate_mode
628    del ArmStore.rigify_force_widget_update
629    del ArmStore.rigify_target_rig
630    del ArmStore.rigify_rig_ui
631    del ArmStore.rigify_rig_basename
632
633    IDStore = bpy.types.WindowManager
634    del IDStore.rigify_collection
635    del IDStore.rigify_types
636    del IDStore.rigify_active_type
637    del IDStore.rigify_transfer_only_selected
638
639    # Classes.
640    for cls in classes:
641        unregister_class(cls)
642
643    clear_rigify_parameters()
644
645    # Sub-modules.
646    metarig_menu.unregister()
647    ui.unregister()
648    feature_set_list.unregister()
649