1 # SPDX-License-Identifier: GPL-2.0-or-later
3 from typing
import TYPE_CHECKING
, List
, Sequence
, Optional
6 from bpy
.props
import StringProperty
11 from zipfile
import ZipFile
12 from shutil
import rmtree
14 from . import feature_sets
17 from . import RigifyFeatureSets
20 DEFAULT_NAME
= 'rigify'
22 # noinspection PyProtectedMember
23 INSTALL_PATH
= feature_sets
._install
_path
()
24 NAME_PREFIX
= feature_sets
.__name
__.split('.')
27 def get_install_path(*, create
=False):
28 if not os
.path
.exists(INSTALL_PATH
):
30 os
.makedirs(INSTALL_PATH
, exist_ok
=True)
37 def get_installed_modules_names() -> List
[str]:
38 """Return a list of module names of all feature sets in the file system."""
39 features_path
= get_install_path()
45 for fs
in os
.listdir(features_path
):
46 if fs
and fs
[0] != '.' and fs
!= DEFAULT_NAME
:
47 fs_path
= os
.path
.join(features_path
, fs
)
48 if os
.path
.isdir(fs_path
):
54 def get_prefs_feature_sets() -> Sequence
['RigifyFeatureSets']:
55 from . import RigifyPreferences
56 return RigifyPreferences
.get_instance().rigify_feature_sets
59 def get_enabled_modules_names() -> List
[str]:
60 """Return a list of module names of all enabled feature sets."""
61 installed_module_names
= get_installed_modules_names()
62 rigify_feature_sets
= get_prefs_feature_sets()
64 enabled_module_names
= {fs
.module_name
for fs
in rigify_feature_sets
if fs
.enabled
}
66 return [name
for name
in installed_module_names
if name
in enabled_module_names
]
69 def get_module(feature_set
: str):
70 return importlib
.import_module('.'.join([*NAME_PREFIX
, feature_set
]))
73 def get_module_safe(feature_set
: str):
74 # noinspection PyBroadException
76 return get_module(feature_set
)
81 def get_dir_path(feature_set
: str, *extra_items
: list[str]):
82 base_dir
= os
.path
.join(INSTALL_PATH
, feature_set
, *extra_items
)
83 base_path
= [*NAME_PREFIX
, feature_set
, *extra_items
]
84 return base_dir
, base_path
87 def get_info_dict(feature_set
: str):
88 module
= get_module_safe(feature_set
)
90 if module
and hasattr(module
, 'rigify_info'):
91 data
= module
.rigify_info
92 if isinstance(data
, dict):
98 def call_function_safe(module_name
: str, func_name
: str,
99 args
: Optional
[list] = None, kwargs
: Optional
[dict] = None):
100 module
= get_module_safe(module_name
)
103 func
= getattr(module
, func_name
, None)
106 # noinspection PyBroadException
108 return func(*(args
or []), **(kwargs
or {}))
110 print(f
"Rigify Error: Could not call function '{func_name}' of feature set "
111 f
"'{module_name}': exception occurred.\n")
112 traceback
.print_exc()
118 def call_register_function(feature_set
: str, do_register
: bool):
119 call_function_safe(feature_set
, 'register' if do_register
else 'unregister')
122 def get_ui_name(feature_set
: str):
123 # Try to get user-defined name
124 info
= get_info_dict(feature_set
)
128 # Default name based on directory
129 name
= re
.sub(r
'[_.-]', ' ', feature_set
)
130 name
= re
.sub(r
'(?<=\d) (?=\d)', '.', name
)
134 def feature_set_items(_scene
, _context
):
135 """Get items for the Feature Set EnumProperty"""
137 ('all', 'All', 'All installed feature sets and rigs bundled with Rigify'),
138 ('rigify', 'Rigify Built-in', 'Rigs bundled with Rigify'),
141 for fs
in get_enabled_modules_names():
142 ui_name
= get_ui_name(fs
)
143 items
.append((fs
, ui_name
, ui_name
))
148 def verify_feature_set_archive(zipfile
):
149 """Verify that the zip file contains one root directory, and some required files."""
154 for name
in zipfile
.namelist():
155 parts
= re
.split(r
'[/\\]', name
)
159 elif dirname
!= parts
[0]:
163 if len(parts
) == 2 and parts
[1] == '__init__.py':
166 if len(parts
) > 2 and parts
[1] in {'rigs', 'metarigs'} and parts
[-1] == '__init__.py':
169 return dirname
, init_found
, data_found
172 # noinspection PyPep8Naming
173 class DATA_OT_rigify_add_feature_set(bpy
.types
.Operator
):
174 bl_idname
= "wm.rigify_add_feature_set"
175 bl_label
= "Add External Feature Set"
176 bl_description
= "Add external feature set (rigs, metarigs, ui templates)"
177 bl_options
= {"REGISTER", "UNDO", "INTERNAL"}
179 filter_glob
: StringProperty(default
="*.zip", options
={'HIDDEN'})
180 filepath
: StringProperty(maxlen
=1024, subtype
='FILE_PATH', options
={'HIDDEN', 'SKIP_SAVE'})
183 def poll(cls
, context
):
186 def invoke(self
, context
, event
):
187 context
.window_manager
.fileselect_add(self
)
188 return {'RUNNING_MODAL'}
190 def execute(self
, context
):
191 from . import RigifyPreferences
192 addon_prefs
= RigifyPreferences
.get_instance()
194 rigify_config_path
= get_install_path(create
=True)
196 with
ZipFile(bpy
.path
.abspath(self
.filepath
), 'r') as zip_archive
:
197 base_dirname
, init_found
, data_found
= verify_feature_set_archive(zip_archive
)
200 self
.report({'ERROR'}, "The feature set archive must contain one base directory.")
203 # Patch up some invalid characters to allow using 'Download ZIP' on GitHub.
204 fixed_dirname
= re
.sub(r
'[.-]', '_', base_dirname
)
206 if not re
.fullmatch(r
'[a-zA-Z][a-zA-Z_0-9]*', fixed_dirname
):
207 self
.report({'ERROR'},
208 f
"The feature set archive base directory name is not a valid "
209 f
"identifier: '{base_dirname}'.")
212 if fixed_dirname
== DEFAULT_NAME
:
214 {'ERROR'}, f
"The '{DEFAULT_NAME}' name is not allowed for feature sets.")
217 if not init_found
or not data_found
:
220 "The feature set archive has no rigs or metarigs, or is missing __init__.py.")
223 base_dir
= os
.path
.join(rigify_config_path
, base_dirname
)
224 fixed_dir
= os
.path
.join(rigify_config_path
, fixed_dirname
)
226 for path
, name
in [(base_dir
, base_dirname
), (fixed_dir
, fixed_dirname
)]:
227 if os
.path
.exists(path
):
228 self
.report({'ERROR'}, f
"Feature set directory already exists: '{name}'.")
231 # Unpack the validated archive and fix the directory name if necessary
232 zip_archive
.extractall(rigify_config_path
)
234 if base_dir
!= fixed_dir
:
235 os
.rename(base_dir
, fixed_dir
)
237 # Call the register callback of the new set
238 call_register_function(fixed_dirname
, True)
240 addon_prefs
.update_external_rigs()
242 addon_prefs
.active_feature_set_index
= len(addon_prefs
.rigify_feature_sets
)-1
247 # noinspection PyPep8Naming
248 class DATA_OT_rigify_remove_feature_set(bpy
.types
.Operator
):
249 bl_idname
= "wm.rigify_remove_feature_set"
250 bl_label
= "Remove External Feature Set"
251 bl_description
= "Remove external feature set (rigs, metarigs, ui templates)"
252 bl_options
= {"REGISTER", "UNDO", "INTERNAL"}
255 def poll(cls
, context
):
258 def invoke(self
, context
, event
):
259 return context
.window_manager
.invoke_confirm(self
, event
)
261 def execute(self
, context
):
262 from . import RigifyPreferences
263 addon_prefs
= RigifyPreferences
.get_instance()
264 feature_set_list
= addon_prefs
.rigify_feature_sets
265 active_idx
= addon_prefs
.active_feature_set_index
266 active_fs
: 'RigifyFeatureSets' = feature_set_list
[active_idx
]
268 # Call the 'unregister' callback of the set being removed.
269 if active_fs
.enabled
:
270 call_register_function(active_fs
.module_name
, do_register
=False)
272 # Remove the feature set's folder from the file system.
273 rigify_config_path
= get_install_path()
274 if rigify_config_path
:
275 set_path
= os
.path
.join(rigify_config_path
, active_fs
.module_name
)
276 if os
.path
.exists(set_path
):
279 # Remove the feature set's entry from the addon preferences.
280 feature_set_list
.remove(active_idx
)
282 # Remove the feature set's entries from the metarigs and rig types.
283 addon_prefs
.update_external_rigs()
285 # Update active index.
286 addon_prefs
.active_feature_set_index
-= 1
292 bpy
.utils
.register_class(DATA_OT_rigify_add_feature_set
)
293 bpy
.utils
.register_class(DATA_OT_rigify_remove_feature_set
)
297 bpy
.utils
.unregister_class(DATA_OT_rigify_add_feature_set
)
298 bpy
.utils
.unregister_class(DATA_OT_rigify_remove_feature_set
)