diff options
-rw-r--r-- | rigify/__init__.py | 32 | ||||
-rw-r--r-- | rigify/feature_set_list.py (renamed from rigify/feature_sets.py) | 120 | ||||
-rw-r--r-- | rigify/feature_sets/__init__.py | 29 | ||||
-rw-r--r-- | rigify/metarig_menu.py | 65 | ||||
-rw-r--r-- | rigify/rig_lists.py | 75 | ||||
-rw-r--r-- | rigify/ui.py | 8 | ||||
-rw-r--r-- | rigify/utils/__init__.py | 4 | ||||
-rw-r--r-- | rigify/utils/rig.py | 32 |
8 files changed, 230 insertions, 135 deletions
diff --git a/rigify/__init__.py b/rigify/__init__.py index 092b882b..8bb09357 100644 --- a/rigify/__init__.py +++ b/rigify/__init__.py @@ -35,11 +35,11 @@ if "bpy" in locals(): importlib.reload(generate) importlib.reload(ui) importlib.reload(utils) + importlib.reload(feature_set_list) importlib.reload(metarig_menu) importlib.reload(rig_lists) - importlib.reload(feature_sets) else: - from . import (utils, rig_lists, generate, ui, metarig_menu, feature_sets) + from . import (utils, feature_set_list, rig_lists, generate, ui, metarig_menu) import bpy import sys @@ -132,18 +132,16 @@ class RigifyPreferences(AddonPreferences): if self.legacy_mode: return - feature_sets_path = os.path.join(bpy.utils.script_path_user(), 'rigify') + set_list = feature_set_list.get_installed_list() - if os.path.exists(feature_sets_path): - if feature_sets_path not in sys.path: - sys.path.append(feature_sets_path) + if len(set_list) > 0: # Reload rigs print('Reloading external rigs...') - rig_lists.get_external_rigs(feature_sets_path) + rig_lists.get_external_rigs(set_list) # Reload metarigs print('Reloading external metarigs...') - metarig_menu.get_external_metarigs(feature_sets_path) + metarig_menu.get_external_metarigs(set_list) # Re-register rig paramaters register_rig_parameters() @@ -198,13 +196,11 @@ class RigifyPreferences(AddonPreferences): op.data_path = 'addon_prefs.show_rigs_folder_expanded' sub.label(text='{}: {}'.format('Rigify', 'External feature sets')) if rigs_expand: - if os.path.exists(os.path.join(bpy.utils.script_path_user(), 'rigify')): - feature_sets_path = os.path.join(bpy.utils.script_path_user(), 'rigify') - for fs in os.listdir(feature_sets_path): - row = col.row() - row.label(text=fs) - op = row.operator("wm.rigify_remove_feature_set", text="Remove", icon='CANCEL') - op.featureset = fs + for fs in feature_set_list.get_installed_list(): + row = col.split(factor=0.8) + row.label(text=feature_set_list.get_ui_name(fs)) + op = row.operator("wm.rigify_remove_feature_set", text="Remove", icon='CANCEL') + op.featureset = fs row = col.row(align=True) row.operator("wm.rigify_add_feature_set", text="Install Feature Set from File...", icon='FILEBROWSER') @@ -372,7 +368,7 @@ def register(): # Sub-modules. ui.register() - feature_sets.register() + feature_set_list.register() metarig_menu.register() # Classes. @@ -383,7 +379,7 @@ def register(): bpy.types.Armature.rigify_layers = CollectionProperty(type=RigifyArmatureLayer) bpy.types.Armature.active_feature_set = EnumProperty( - items=feature_sets.feature_set_items, + items=feature_set_list.feature_set_items, name="Feature Set", description="Restrict the rig list to a specific custom feature set" ) @@ -538,4 +534,4 @@ def unregister(): # Sub-modules. metarig_menu.unregister() ui.unregister() - feature_sets.unregister() + feature_set_list.unregister() diff --git a/rigify/feature_sets.py b/rigify/feature_set_list.py index 3a7c2bec..fb7075eb 100644 --- a/rigify/feature_sets.py +++ b/rigify/feature_set_list.py @@ -20,21 +20,95 @@ import bpy from bpy.props import StringProperty import os import re +import importlib from zipfile import ZipFile from shutil import rmtree +from . import feature_sets + + +DEFAULT_NAME = 'rigify' + +INSTALL_PATH = feature_sets._install_path() +NAME_PREFIX = feature_sets.__name__.split('.') + + +def get_install_path(*, create=False): + if not os.path.exists(INSTALL_PATH): + if create: + os.makedirs(INSTALL_PATH, exist_ok=True) + else: + return None + + return INSTALL_PATH + + +def get_installed_list(): + features_path = get_install_path() + if not features_path: + return [] + + sets = [] + + for fs in os.listdir(features_path): + if fs and fs[0] != '.' and fs != DEFAULT_NAME: + fs_path = os.path.join(features_path, fs) + if os.path.isdir(fs_path): + sets.append(fs) + + return sets + + +def get_module(feature_set): + return importlib.import_module('.'.join([*NAME_PREFIX, feature_set])) + + +def get_module_safe(feature_set): + try: + return get_module(feature_set) + except: + return None + + +def get_dir_path(feature_set, *extra_items): + base_dir = os.path.join(INSTALL_PATH, feature_set, *extra_items) + base_path = [*NAME_PREFIX, feature_set, *extra_items] + return base_dir, base_path + + +def get_info_dict(feature_set): + module = get_module_safe(feature_set) + + if module and hasattr(module, 'rigify_info'): + data = module.rigify_info + if isinstance(data, dict): + return data + + return {} + + +def get_ui_name(feature_set): + # Try to get user-defined name + info = get_info_dict(feature_set) + if 'name' in info: + return info['name'] + + # Default name based on directory + name = re.sub(r'[_.-]', ' ', feature_set) + name = re.sub(r'(?<=\d) (?=\d)', '.', name) + return name.title() + def feature_set_items(scene, context): """Get items for the Feature Set EnumProperty""" - feature_sets_path = os.path.join( - bpy.utils.script_path_user(), 'rigify') items = [('all',)*3, ('rigify',)*3, ] - if os.path.exists(feature_sets_path): - for fs in os.listdir(feature_sets_path): - items.append((fs,)*3) + + for fs in get_installed_list(): + items.append((fs,)*3) return items + def verify_feature_set_archive(zipfile): """Verify that the zip file contains one root directory, and some required files.""" dirname = None @@ -58,6 +132,7 @@ def verify_feature_set_archive(zipfile): return dirname, init_found, data_found + class DATA_OT_rigify_add_feature_set(bpy.types.Operator): bl_idname = "wm.rigify_add_feature_set" bl_label = "Add External Feature Set" @@ -78,8 +153,8 @@ class DATA_OT_rigify_add_feature_set(bpy.types.Operator): def execute(self, context): addon_prefs = context.preferences.addons[__package__].preferences - rigify_config_path = os.path.join(bpy.utils.script_path_user(), 'rigify') - os.makedirs(rigify_config_path, exist_ok=True) + rigify_config_path = get_install_path(create=True) + with ZipFile(bpy.path.abspath(self.filepath), 'r') as zip_archive: base_dirname, init_found, data_found = verify_feature_set_archive(zip_archive) @@ -87,16 +162,35 @@ class DATA_OT_rigify_add_feature_set(bpy.types.Operator): self.report({'ERROR'}, "The feature set archive must contain one base directory.") return {'CANCELLED'} - if not re.fullmatch(r'[a-zA-Z_][a-zA-Z_0-9-]*', base_dirname): - self.report({'ERROR'}, "The feature set archive has invalid characters in the base directory name: '%s'." % (base_dirname)) + # Patch up some invalid characters to allow using 'Download ZIP' on GitHub. + fixed_dirname = re.sub(r'[.-]', '_', base_dirname) + + if not re.fullmatch(r'[a-zA-Z][a-zA-Z_0-9]*', fixed_dirname): + self.report({'ERROR'}, "The feature set archive base directory name is not a valid identifier: '%s'." % (base_dirname)) + return {'CANCELLED'} + + if fixed_dirname == DEFAULT_NAME: + self.report({'ERROR'}, "The '%s' name is not allowed for feature sets." % (DEFAULT_NAME)) return {'CANCELLED'} if not init_found or not data_found: self.report({'ERROR'}, "The feature set archive has no rigs or metarigs, or is missing __init__.py.") return {'CANCELLED'} + base_dir = os.path.join(rigify_config_path, base_dirname) + fixed_dir = os.path.join(rigify_config_path, fixed_dirname) + + for path, name in [(base_dir, base_dirname), (fixed_dir, fixed_dirname)]: + if os.path.exists(path): + self.report({'ERROR'}, "Feature set directory already exists: '%s'." % (name)) + return {'CANCELLED'} + + # Unpack the validated archive and fix the directory name if necessary zip_archive.extractall(rigify_config_path) + if base_dir != fixed_dir: + os.rename(base_dir, fixed_dir) + addon_prefs.machin = bpy.props.EnumProperty(items=(('a',)*3, ('b',)*3, ('c',)*3),) addon_prefs.update_external_rigs() @@ -121,9 +215,11 @@ class DATA_OT_rigify_remove_feature_set(bpy.types.Operator): def execute(self, context): addon_prefs = context.preferences.addons[__package__].preferences - rigify_config_path = os.path.join(bpy.utils.script_path_user(), 'rigify') - if os.path.exists(os.path.join(rigify_config_path, self.featureset)): - rmtree(os.path.join(rigify_config_path, self.featureset)) + rigify_config_path = get_install_path() + if rigify_config_path: + set_path = os.path.join(rigify_config_path, self.featureset) + if os.path.exists(set_path): + rmtree(set_path) addon_prefs.update_external_rigs() return {'FINISHED'} diff --git a/rigify/feature_sets/__init__.py b/rigify/feature_sets/__init__.py new file mode 100644 index 00000000..e9981d52 --- /dev/null +++ b/rigify/feature_sets/__init__.py @@ -0,0 +1,29 @@ +#====================== BEGIN GPL LICENSE BLOCK ====================== +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +#======================= END GPL LICENSE BLOCK ======================== + +# Redirect the module loader to the user scripts directory. + +# Thus feature set modules can be added to this package without +# writing to the actual Rigify installation directory. + +def _install_path(): + import bpy + import os + return os.path.join(bpy.utils.script_path_user(), 'rigify') + +__path__ = [ _install_path() ] diff --git a/rigify/metarig_menu.py b/rigify/metarig_menu.py index 2c8adac7..dc583a67 100644 --- a/rigify/metarig_menu.py +++ b/rigify/metarig_menu.py @@ -22,10 +22,12 @@ import os import traceback from string import capwords +from collections import defaultdict import bpy from . import utils +from . import feature_set_list class ArmatureSubMenu(bpy.types.Menu): @@ -39,22 +41,22 @@ class ArmatureSubMenu(bpy.types.Menu): layout.operator(op, icon='OUTLINER_OB_ARMATURE', text=text) -def get_metarigs(base_path, path, depth=0): +def get_metarigs(metarigs, base_dir, base_path, *, path=[], nested=False): """ Searches for metarig modules, and returns a list of the imported modules. """ - metarigs = {} + dir_path = os.path.join(base_dir, *path) try: - files = os.listdir(os.path.join(base_path, path)) + files = os.listdir(dir_path) except FileNotFoundError: files = [] files.sort() for f in files: - is_dir = os.path.isdir(os.path.join(base_path, path, f)) # Whether the file is a directory + is_dir = os.path.isdir(os.path.join(dir_path, f)) # Whether the file is a directory # Stop cases if f[0] in [".", "_"]: @@ -64,19 +66,15 @@ def get_metarigs(base_path, path, depth=0): continue if is_dir: # Check directories - # Check for sub-metarigs - metarigs[f] = get_metarigs(base_path, os.path.join(path, f, ""), depth=1) # "" adds a final slash + get_metarigs(metarigs[f], base_dir, base_path, path=[*path, f], nested=True) # "" adds a final slash elif f.endswith(".py"): # Check straight-up python files f = f[:-3] - module_name = os.path.join(path, f).replace(os.sep, ".") - metarig_module = utils.get_resource(module_name, base_path=base_path) - if depth == 1: - metarigs[f] = metarig_module + module = utils.get_resource('.'.join([*base_path, *path, f])) + if nested: + metarigs[f] = module else: - metarigs[utils.METARIG_DIR] = {f: metarig_module} - - return metarigs + metarigs[utils.METARIG_DIR][f] = module def make_metarig_add_execute(m): @@ -119,11 +117,15 @@ def make_submenu_func(bl_idname, text): # Get the metarig modules def get_internal_metarigs(): - MODULE_DIR = os.path.dirname(os.path.dirname(__file__)) + BASE_RIGIFY_DIR = os.path.dirname(__file__) + BASE_RIGIFY_PATH = __name__.split('.')[:-1] + + get_metarigs(metarigs, os.path.join(BASE_RIGIFY_DIR, utils.METARIG_DIR), [*BASE_RIGIFY_PATH, utils.METARIG_DIR]) - metarigs.update(get_metarigs(MODULE_DIR, os.path.join(os.path.basename(os.path.dirname(__file__)), utils.METARIG_DIR, ''))) +def infinite_defaultdict(): + return defaultdict(infinite_defaultdict) -metarigs = {} +metarigs = infinite_defaultdict() metarig_ops = {} armature_submenus = [] menu_funcs = [] @@ -212,29 +214,24 @@ def unregister(): for mf in menu_funcs: bpy.types.VIEW3D_MT_armature_add.remove(mf) -def get_external_metarigs(feature_sets_path): +def get_external_metarigs(set_list): unregister() # Clear and fill metarigs public variables metarigs.clear() get_internal_metarigs() - metarigs['external'] = {} - - for feature_set in os.listdir(feature_sets_path): - if feature_set: - try: - try: - utils.get_resource(os.path.join(feature_set, '__init__'), feature_sets_path) - except FileNotFoundError: - print("Rigify Error: Could not load feature set '%s': __init__.py not found.\n" % (feature_set)) - continue - - metarigs['external'].update(get_metarigs(feature_sets_path, os.path.join(feature_set, utils.METARIG_DIR))) - except Exception: - print("Rigify Error: Could not load feature set '%s' metarigs: exception occurred.\n" % (feature_set)) - traceback.print_exc() - print("") - continue + + + for feature_set in set_list: + try: + base_dir, base_path = feature_set_list.get_dir_path(feature_set, utils.METARIG_DIR) + + get_metarigs(metarigs['external'], base_dir, base_path) + except Exception: + print("Rigify Error: Could not load feature set '%s' metarigs: exception occurred.\n" % (feature_set)) + traceback.print_exc() + print("") + continue metarig_ops.clear() armature_submenus.clear() diff --git a/rigify/rig_lists.py b/rigify/rig_lists.py index fac88f1a..0045b185 100644 --- a/rigify/rig_lists.py +++ b/rigify/rig_lists.py @@ -20,9 +20,10 @@ import os import traceback from . import utils +from . import feature_set_list -def get_rigs(base_path, path, feature_set='rigify'): +def get_rigs(base_dir, base_path, *, path=[], feature_set=feature_set_list.DEFAULT_NAME): """ Recursively searches for rig types, and returns a list. :param base_path: base dir where rigs are stored @@ -34,74 +35,74 @@ def get_rigs(base_path, path, feature_set='rigify'): rigs = {} impl_rigs = {} + dir_path = os.path.join(base_dir, *path) + try: - files = os.listdir(os.path.join(base_path, path)) + files = os.listdir(dir_path) except FileNotFoundError: files = [] files.sort() for f in files: - is_dir = os.path.isdir(os.path.join(base_path, path, f)) # Whether the file is a directory + is_dir = os.path.isdir(os.path.join(dir_path, f)) # Whether the file is a directory # Stop cases if f[0] in [".", "_"]: continue if f.count(".") >= 2 or (is_dir and "." in f): - print("Warning: %r, filename contains a '.', skipping" % os.path.join(path, f)) + print("Warning: %r, filename contains a '.', skipping" % os.path.join(*base_path, *path, f)) continue if is_dir: - # Check directories - module_name = os.path.join(path, "__init__").replace(os.sep, ".") # Check for sub-rigs - sub_rigs, sub_impls = get_rigs(base_path, os.path.join(path, f, ""), feature_set) # "" adds a final slash - rigs.update({"%s.%s" % (f, l): sub_rigs[l] for l in sub_rigs}) - impl_rigs.update({"%s.%s" % (f, l): sub_rigs[l] for l in sub_impls}) + sub_rigs, sub_impls = get_rigs(base_dir, base_path, path=[*path, f], feature_set=feature_set) + rigs.update(sub_rigs) + impl_rigs.update(sub_impls) elif f.endswith(".py"): # Check straight-up python files - f = f[:-3] - module_name = os.path.join(path, f).replace(os.sep, ".") - rig_module = utils.get_resource(module_name, base_path=base_path) + subpath = [*path, f[:-3]] + key = '.'.join(subpath) + rig_module = utils.get_resource('.'.join(base_path + subpath)) if hasattr(rig_module, "Rig"): - rigs[f] = {"module": rig_module, - "feature_set": feature_set} + rigs[key] = {"module": rig_module, + "feature_set": feature_set} if hasattr(rig_module, 'IMPLEMENTATION') and rig_module.IMPLEMENTATION: - impl_rigs[f] = rig_module + impl_rigs[key] = rig_module return rigs, impl_rigs # Public variables -MODULE_DIR = os.path.dirname(os.path.dirname(__file__)) +def get_internal_rigs(): + BASE_RIGIFY_DIR = os.path.dirname(__file__) + BASE_RIGIFY_PATH = __name__.split('.')[:-1] + + return get_rigs(os.path.join(BASE_RIGIFY_DIR, utils.RIG_DIR), [*BASE_RIGIFY_PATH, utils.RIG_DIR]) + -rigs, implementation_rigs = get_rigs(MODULE_DIR, os.path.join(os.path.basename(os.path.dirname(__file__)), utils.RIG_DIR, '')) +rigs, implementation_rigs = get_internal_rigs() -def get_external_rigs(feature_sets_path): +def get_external_rigs(set_list): # Clear and fill rigify rigs and implementation rigs public variables for rig in list(rigs.keys()): - if rigs[rig]["feature_set"] != "rigify": + if rigs[rig]["feature_set"] != feature_set_list.DEFAULT_NAME: rigs.pop(rig) if rig in implementation_rigs: implementation_rigs.pop(rig) # Get external rigs - for feature_set in os.listdir(feature_sets_path): - if feature_set: - try: - try: - utils.get_resource(os.path.join(feature_set, '__init__'), feature_sets_path) - except FileNotFoundError: - print("Rigify Error: Could not load feature set '%s': __init__.py not found.\n" % (feature_set)) - continue - - external_rigs, external_impl_rigs = get_rigs(feature_sets_path, os.path.join(feature_set, utils.RIG_DIR), feature_set) - except Exception: - print("Rigify Error: Could not load feature set '%s' rigs: exception occurred.\n" % (feature_set)) - traceback.print_exc() - print("") - continue - - rigs.update(external_rigs) - implementation_rigs.update(external_impl_rigs) + for feature_set in set_list: + try: + base_dir, base_path = feature_set_list.get_dir_path(feature_set, utils.RIG_DIR) + + external_rigs, external_impl_rigs = get_rigs(base_dir, base_path, feature_set=feature_set) + except Exception: + print("Rigify Error: Could not load feature set '%s' rigs: exception occurred.\n" % (feature_set)) + traceback.print_exc() + print("") + continue + + rigs.update(external_rigs) + implementation_rigs.update(external_impl_rigs) diff --git a/rigify/ui.py b/rigify/ui.py index 48f34e0e..35b9c847 100644 --- a/rigify/ui.py +++ b/rigify/ui.py @@ -38,7 +38,7 @@ from .rigs.utils import get_limb_generated_names from . import rig_lists from . import generate from . import rot_mode -from . import feature_sets +from . import feature_set_list def build_type_list(context, rigify_types): @@ -46,7 +46,7 @@ def build_type_list(context, rigify_types): for r in sorted(rig_lists.rigs): if (context.object.data.active_feature_set in ('all', rig_lists.rigs[r]['feature_set']) - or len(feature_sets.feature_set_items(context.scene, context)) == 2 + or len(feature_set_list.feature_set_items(context.scene, context)) == 2 ): a = rigify_types.add() a.name = r @@ -185,7 +185,7 @@ class DATA_PT_rigify_buttons(bpy.types.Panel): id_store.rigify_active_type = 0 # Rig type list - if len(feature_sets.feature_set_items(context.scene, context)) > 2: + if len(feature_set_list.feature_set_items(context.scene, context)) > 2: row = layout.row() row.prop(context.object.data, "active_feature_set") row = layout.row() @@ -598,7 +598,7 @@ class BONE_PT_rigify_buttons(bpy.types.Panel): build_type_list(context, id_store.rigify_types) # Rig type field - if len(feature_sets.feature_set_items(context.scene, context)) > 2: + if len(feature_set_list.feature_set_items(context.scene, context)) > 2: row = layout.row() row.prop(context.object.data, "active_feature_set") row = layout.row() diff --git a/rigify/utils/__init__.py b/rigify/utils/__init__.py index da4689a9..9fa6a3d2 100644 --- a/rigify/utils/__init__.py +++ b/rigify/utils/__init__.py @@ -24,8 +24,8 @@ from .widgets_special import create_neck_bend_widget, create_neck_tweak_widget from .animation import get_keyed_frames, bones_in_frame, overwrite_prop_animation -from .rig import RIG_DIR, METARIG_DIR, MODULE_NAME, TEMPLATE_DIR, outdated_types, upgradeMetarigTypes -from .rig import get_rig_type, get_metarig_module, write_metarig, get_resource +from .rig import RIG_DIR, METARIG_DIR, TEMPLATE_DIR, outdated_types, upgradeMetarigTypes +from .rig import write_metarig, get_resource from .rig import connected_children_names, has_connected_children from .layers import get_layers, ControlLayersOption diff --git a/rigify/utils/rig.py b/rigify/utils/rig.py index 9aeb9e0f..414ea133 100644 --- a/rigify/utils/rig.py +++ b/rigify/utils/rig.py @@ -27,7 +27,6 @@ RIG_DIR = "rigs" # Name of the directory where rig types are kept METARIG_DIR = "metarigs" # Name of the directory where metarigs are kept TEMPLATE_DIR = "ui_templates" # Name of the directory where ui templates are kept -MODULE_NAME = "rigify" # Windows/Mac blender is weird, so __package__ doesn't work outdated_types = {"pitchipoy.limbs.super_limb": "limbs.super_limb", "pitchipoy.limbs.super_arm": "limbs.super_limb", @@ -81,36 +80,13 @@ def upgradeMetarigTypes(metarig, revert=False): # Misc #============================================= -def get_rig_type(rig_type, base_path=''): - return get_resource(rig_type, base_path=base_path) - -def get_resource(resource_name, base_path=''): - """ Fetches a rig module by name, and returns it. - """ - - if '.' in resource_name: - module_subpath = str.join(os.sep, resource_name.split('.')) - package = resource_name.split('.')[0] - for sub in resource_name.split('.')[1:]: - package = '.'.join([package, sub]) - submod = importlib.import_module(package) - else: - module_subpath = resource_name - - spec = importlib.util.spec_from_file_location(resource_name, os.path.join(base_path, module_subpath + '.py')) - submod = importlib.util.module_from_spec(spec) - spec.loader.exec_module(submod) - return submod - - -def get_metarig_module(metarig_name, path=METARIG_DIR): +def get_resource(resource_name): """ Fetches a rig module by name, and returns it. """ - name = ".%s.%s" % (path, metarig_name) - submod = importlib.import_module(name, package=MODULE_NAME) - importlib.reload(submod) - return submod + module = importlib.import_module(resource_name) + importlib.reload(module) + return module def connected_children_names(obj, bone_name): |