1 # SPDX-License-Identifier: GPL-2.0-or-later
3 from typing
import List
6 from bpy
.props
import StringProperty
11 from zipfile
import ZipFile
12 from shutil
import rmtree
14 from . import feature_sets
17 DEFAULT_NAME
= 'rigify'
19 INSTALL_PATH
= feature_sets
._install
_path
()
20 NAME_PREFIX
= feature_sets
.__name
__.split('.')
23 def get_install_path(*, create
=False):
24 if not os
.path
.exists(INSTALL_PATH
):
26 os
.makedirs(INSTALL_PATH
, exist_ok
=True)
33 def get_installed_modules_names() -> List
[str]:
34 """Return a list of module names of all feature sets in the file system."""
35 features_path
= get_install_path()
41 for fs
in os
.listdir(features_path
):
42 if fs
and fs
[0] != '.' and fs
!= DEFAULT_NAME
:
43 fs_path
= os
.path
.join(features_path
, fs
)
44 if os
.path
.isdir(fs_path
):
50 def get_enabled_modules_names() -> List
[str]:
51 """Return a list of module names of all enabled feature sets."""
52 rigify_prefs
= bpy
.context
.preferences
.addons
[__package__
].preferences
53 installed_module_names
= get_installed_modules_names()
54 rigify_feature_sets
= rigify_prefs
.rigify_feature_sets
56 enabled_module_names
= { fs
.module_name
for fs
in rigify_feature_sets
if fs
.enabled
}
58 return [name
for name
in installed_module_names
if name
in enabled_module_names
]
61 def get_module(feature_set
):
62 return importlib
.import_module('.'.join([*NAME_PREFIX
, feature_set
]))
65 def get_module_safe(feature_set
):
67 return get_module(feature_set
)
72 def get_dir_path(feature_set
, *extra_items
):
73 base_dir
= os
.path
.join(INSTALL_PATH
, feature_set
, *extra_items
)
74 base_path
= [*NAME_PREFIX
, feature_set
, *extra_items
]
75 return base_dir
, base_path
78 def get_info_dict(feature_set
):
79 module
= get_module_safe(feature_set
)
81 if module
and hasattr(module
, 'rigify_info'):
82 data
= module
.rigify_info
83 if isinstance(data
, dict):
89 def call_function_safe(module_name
, func_name
, args
=[], kwargs
={}):
90 module
= get_module_safe(module_name
)
93 func
= getattr(module
, func_name
, None)
97 return func(*args
, **kwargs
)
99 print(f
"Rigify Error: Could not call function '{func_name}' of feature set '{module_name}': exception occurred.\n")
100 traceback
.print_exc()
106 def call_register_function(feature_set
, register
):
107 call_function_safe(feature_set
, 'register' if register
else 'unregister')
110 def get_ui_name(feature_set
):
111 # Try to get user-defined name
112 info
= get_info_dict(feature_set
)
116 # Default name based on directory
117 name
= re
.sub(r
'[_.-]', ' ', feature_set
)
118 name
= re
.sub(r
'(?<=\d) (?=\d)', '.', name
)
122 def feature_set_items(scene
, context
):
123 """Get items for the Feature Set EnumProperty"""
125 ('all', 'All', 'All installed feature sets and rigs bundled with Rigify'),
126 ('rigify', 'Rigify Built-in', 'Rigs bundled with Rigify'),
129 for fs
in get_enabled_modules_names():
130 ui_name
= get_ui_name(fs
)
131 items
.append((fs
, ui_name
, ui_name
))
136 def verify_feature_set_archive(zipfile
):
137 """Verify that the zip file contains one root directory, and some required files."""
142 for name
in zipfile
.namelist():
143 parts
= re
.split(r
'[/\\]', name
)
147 elif dirname
!= parts
[0]:
151 if len(parts
) == 2 and parts
[1] == '__init__.py':
154 if len(parts
) > 2 and parts
[1] in {'rigs', 'metarigs'} and parts
[-1] == '__init__.py':
157 return dirname
, init_found
, data_found
160 class DATA_OT_rigify_add_feature_set(bpy
.types
.Operator
):
161 bl_idname
= "wm.rigify_add_feature_set"
162 bl_label
= "Add External Feature Set"
163 bl_description
= "Add external feature set (rigs, metarigs, ui templates)"
164 bl_options
= {"REGISTER", "UNDO", "INTERNAL"}
166 filter_glob
: StringProperty(default
="*.zip", options
={'HIDDEN'})
167 filepath
: StringProperty(maxlen
=1024, subtype
='FILE_PATH', options
={'HIDDEN', 'SKIP_SAVE'})
170 def poll(cls
, context
):
173 def invoke(self
, context
, event
):
174 context
.window_manager
.fileselect_add(self
)
175 return {'RUNNING_MODAL'}
177 def execute(self
, context
):
178 addon_prefs
= context
.preferences
.addons
[__package__
].preferences
180 rigify_config_path
= get_install_path(create
=True)
182 with
ZipFile(bpy
.path
.abspath(self
.filepath
), 'r') as zip_archive
:
183 base_dirname
, init_found
, data_found
= verify_feature_set_archive(zip_archive
)
186 self
.report({'ERROR'}, "The feature set archive must contain one base directory.")
189 # Patch up some invalid characters to allow using 'Download ZIP' on GitHub.
190 fixed_dirname
= re
.sub(r
'[.-]', '_', base_dirname
)
192 if not re
.fullmatch(r
'[a-zA-Z][a-zA-Z_0-9]*', fixed_dirname
):
193 self
.report({'ERROR'}, "The feature set archive base directory name is not a valid identifier: '%s'." % (base_dirname
))
196 if fixed_dirname
== DEFAULT_NAME
:
197 self
.report({'ERROR'}, "The '%s' name is not allowed for feature sets." % (DEFAULT_NAME
))
200 if not init_found
or not data_found
:
201 self
.report({'ERROR'}, "The feature set archive has no rigs or metarigs, or is missing __init__.py.")
204 base_dir
= os
.path
.join(rigify_config_path
, base_dirname
)
205 fixed_dir
= os
.path
.join(rigify_config_path
, fixed_dirname
)
207 for path
, name
in [(base_dir
, base_dirname
), (fixed_dir
, fixed_dirname
)]:
208 if os
.path
.exists(path
):
209 self
.report({'ERROR'}, "Feature set directory already exists: '%s'." % (name
))
212 # Unpack the validated archive and fix the directory name if necessary
213 zip_archive
.extractall(rigify_config_path
)
215 if base_dir
!= fixed_dir
:
216 os
.rename(base_dir
, fixed_dir
)
218 # Call the register callback of the new set
219 call_register_function(fixed_dirname
, True)
221 addon_prefs
.update_external_rigs()
223 addon_prefs
.active_feature_set_index
= len(addon_prefs
.rigify_feature_sets
)-1
228 class DATA_OT_rigify_remove_feature_set(bpy
.types
.Operator
):
229 bl_idname
= "wm.rigify_remove_feature_set"
230 bl_label
= "Remove External Feature Set"
231 bl_description
= "Remove external feature set (rigs, metarigs, ui templates)"
232 bl_options
= {"REGISTER", "UNDO", "INTERNAL"}
235 def poll(cls
, context
):
238 def invoke(self
, context
, event
):
239 return context
.window_manager
.invoke_confirm(self
, event
)
241 def execute(self
, context
):
242 addon_prefs
= context
.preferences
.addons
[__package__
].preferences
243 feature_sets
= addon_prefs
.rigify_feature_sets
244 active_idx
= addon_prefs
.active_feature_set_index
245 active_fs
= feature_sets
[active_idx
]
247 # Call the unregister callback of the set being removed.
248 if active_fs
.enabled
:
249 call_register_function(active_fs
.module_name
, register
=False)
251 # Remove the feature set's folder from the file system.
252 rigify_config_path
= get_install_path()
253 if rigify_config_path
:
254 set_path
= os
.path
.join(rigify_config_path
, active_fs
.module_name
)
255 if os
.path
.exists(set_path
):
258 # Remove the feature set's entry from the addon preferences.
259 feature_sets
.remove(active_idx
)
261 # Remove the feature set's entries from the metarigs and rig types.
262 addon_prefs
.update_external_rigs()
264 # Update active index.
265 addon_prefs
.active_feature_set_index
-= 1
271 bpy
.utils
.register_class(DATA_OT_rigify_add_feature_set
)
272 bpy
.utils
.register_class(DATA_OT_rigify_remove_feature_set
)
275 bpy
.utils
.unregister_class(DATA_OT_rigify_add_feature_set
)
276 bpy
.utils
.unregister_class(DATA_OT_rigify_remove_feature_set
)