Add Easy_Weight
to Addons
#47
@ -32,6 +32,8 @@ from . import force_apply_mirror
|
||||
from . import toggle_weight_paint
|
||||
from . import change_brush
|
||||
from . import weight_paint_context_menu
|
||||
from . import vertex_group_operators
|
||||
from . import vertex_group_menu
|
||||
|
||||
# Each module is expected to have a register() and unregister() function.
|
||||
modules = [
|
||||
@ -39,7 +41,9 @@ modules = [
|
||||
force_apply_mirror,
|
||||
toggle_weight_paint,
|
||||
change_brush,
|
||||
weight_paint_context_menu
|
||||
weight_paint_context_menu,
|
||||
vertex_group_operators,
|
||||
vertex_group_menu
|
||||
]
|
||||
|
||||
class EasyWeightPreferences(AddonPreferences):
|
||||
|
0
utils/__init__.py
Normal file
0
utils/__init__.py
Normal file
169
utils/naming.py
Normal file
169
utils/naming.py
Normal file
@ -0,0 +1,169 @@
|
||||
from typing import Tuple, List, Optional
|
||||
import re
|
||||
|
||||
separators = "-_."
|
||||
|
||||
def get_name(thing) -> str:
|
||||
if hasattr(thing, 'name'):
|
||||
return thing.name
|
||||
else:
|
||||
return str(thing)
|
||||
|
||||
|
||||
def make_name(prefixes=[], base="", suffixes=[],
|
||||
prefix_separator="-", suffix_separator=".") -> str:
|
||||
"""Make a name from a list of prefixes, a base, and a list of suffixes."""
|
||||
name = ""
|
||||
for pre in prefixes:
|
||||
if pre=="": continue
|
||||
name += pre + prefix_separator
|
||||
name += base
|
||||
for suf in suffixes:
|
||||
if suf=="": continue
|
||||
name += suffix_separator + suf
|
||||
return name
|
||||
|
||||
def slice_name(name, prefix_separator="-", suffix_separator="."):
|
||||
"""Break up a name into its prefix, base, suffix components."""
|
||||
prefixes = name.split(prefix_separator)[:-1]
|
||||
suffixes = name.split(suffix_separator)[1:]
|
||||
base = name.split(prefix_separator)[-1].split(suffix_separator)[0]
|
||||
return [prefixes, base, suffixes]
|
||||
|
||||
|
||||
def has_trailing_zeroes(thing):
|
||||
name = get_name(thing)
|
||||
regex = "\.[0-9][0-9][0-9]$"
|
||||
search = re.search(regex, name)
|
||||
return search != None
|
||||
|
||||
def strip_trailing_numbers(name) -> Tuple[str, str]:
|
||||
if "." in name:
|
||||
# Check if there are only digits after the last period
|
||||
slices = name.split(".")
|
||||
after_last_period = slices[-1]
|
||||
before_last_period = ".".join(slices[:-1])
|
||||
|
||||
# If there are only digits after the last period, discard them
|
||||
if all([c in "0123456789" for c in after_last_period]):
|
||||
return before_last_period, "."+after_last_period
|
||||
|
||||
return name, ""
|
||||
|
||||
def get_side_lists(with_separators=False) -> Tuple[List[str], List[str], List[str]]:
|
||||
left = ['left', 'Left', 'LEFT', 'l', 'L',]
|
||||
right_placehold = ['*rgt*', '*Rgt*', '*RGT*', '*r*', '*R*']
|
||||
right = ['right', 'Right', 'RIGHT', 'r', 'R']
|
||||
|
||||
# If the name is longer than 2 characters, only swap side identifiers if they
|
||||
# are next to a separator.
|
||||
if with_separators:
|
||||
for l in [left, right_placehold, right]:
|
||||
l_copy = l[:]
|
||||
for side in l_copy:
|
||||
if len(side)<4:
|
||||
l.remove(side)
|
||||
for sep in separators:
|
||||
l.append(side+sep)
|
||||
l.append(sep+side)
|
||||
|
||||
return left, right_placehold, right
|
||||
|
||||
def flip_name(from_name, ignore_base=True, must_change=False) -> str:
|
||||
"""Turn a left-sided name into a right-sided one or vice versa.
|
||||
|
||||
Based on BLI_string_flip_side_name:
|
||||
https://developer.blender.org/diffusion/B/browse/master/source/blender/blenlib/intern/string_utils.c
|
||||
|
||||
ignore_base: When True, ignore occurrences of side hints unless they're in
|
||||
the beginning or end of the name string.
|
||||
must_change: When True, raise an error if the name couldn't be flipped.
|
||||
"""
|
||||
|
||||
# Handling .### cases
|
||||
stripped_name, number_suffix = strip_trailing_numbers(from_name)
|
||||
|
||||
def flip_sides(list_from, list_to, name):
|
||||
for side_idx, side in enumerate(list_from):
|
||||
opp_side = list_to[side_idx]
|
||||
if ignore_base:
|
||||
# Only look at prefix/suffix.
|
||||
if name.startswith(side):
|
||||
name = name[len(side):]+opp_side
|
||||
break
|
||||
elif name.endswith(side):
|
||||
name = name[:-len(side)]+opp_side
|
||||
break
|
||||
else:
|
||||
# When it comes to searching the middle of a string,
|
||||
# sides must strictly be a full word or separated with "."
|
||||
# otherwise we would catch stuff like "_leg" and turn it into "_reg".
|
||||
if not any([char not in side for char in "-_."]):
|
||||
# Replace all occurences and continue checking for keywords.
|
||||
name = name.replace(side, opp_side)
|
||||
continue
|
||||
return name
|
||||
|
||||
with_separators = len(stripped_name)>2
|
||||
left, right_placehold, right = get_side_lists(with_separators)
|
||||
flipped_name = flip_sides(left, right_placehold, stripped_name)
|
||||
flipped_name = flip_sides(right, left, flipped_name)
|
||||
flipped_name = flip_sides(right_placehold, right, flipped_name)
|
||||
|
||||
# Re-add trailing digits (.###)
|
||||
new_name = flipped_name + number_suffix
|
||||
|
||||
if must_change:
|
||||
assert new_name != from_name, "Failed to flip string: " + from_name
|
||||
|
||||
return new_name
|
||||
|
||||
def side_is_left(name) -> Optional[bool]:
|
||||
"""Identify whether a name belongs to the left or right side or neither."""
|
||||
|
||||
flipped_name = flip_name(name)
|
||||
if flipped_name==name: return None # Return None to indicate neither side.
|
||||
|
||||
stripped_name, number_suffix = strip_trailing_numbers(name)
|
||||
|
||||
def check_start_side(side_list, name):
|
||||
for side in side_list:
|
||||
if name.startswith(side):
|
||||
return True
|
||||
return False
|
||||
|
||||
def check_end_side(side_list, name):
|
||||
for side in side_list:
|
||||
if name.endswith(side):
|
||||
return True
|
||||
return False
|
||||
|
||||
left, right_placehold, right = get_side_lists(with_separators=True)
|
||||
|
||||
is_left_prefix = check_start_side(left, stripped_name)
|
||||
is_left_suffix = check_end_side(left, stripped_name)
|
||||
|
||||
is_right_prefix = check_start_side(right, stripped_name)
|
||||
is_right_suffix = check_end_side(right, stripped_name)
|
||||
|
||||
# Prioritize suffix for determining the name's side.
|
||||
if is_left_suffix or is_right_suffix:
|
||||
return is_left_suffix
|
||||
|
||||
# If no relevant suffix found, try prefix.
|
||||
if is_left_prefix or is_right_prefix:
|
||||
return is_left_prefix
|
||||
|
||||
# If no relevant suffix or prefix found, try anywhere.
|
||||
any_left = any([side in name for side in left])
|
||||
any_right = any([side in name for side in right])
|
||||
if not any_left and not any_right:
|
||||
# If neither side found, return None.
|
||||
return None
|
||||
if any_left and not any_right:
|
||||
return True
|
||||
if any_right and not any_left:
|
||||
return False
|
||||
|
||||
# If left and right were both found somewhere, I give up.
|
||||
return None
|
127
vertex_group_menu.py
Normal file
127
vertex_group_menu.py
Normal file
@ -0,0 +1,127 @@
|
||||
import bpy
|
||||
from .vertex_group_operators import (
|
||||
DeleteEmptyDeformGroups,
|
||||
FocusDeformBones,
|
||||
DeleteUnselectedDeformGroups,
|
||||
DeleteUnusedVertexGroups,
|
||||
CreateMirrorGroups,
|
||||
)
|
||||
|
||||
class MESH_MT_vertex_group_batch_delete(bpy.types.Menu):
|
||||
bl_label = "Batch Delete"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.operator("object.vertex_group_remove", text="Delete All Groups").all = True
|
||||
layout.operator("object.vertex_group_remove", text="Delete All Unlocked Groups").all_unlocked = True
|
||||
layout.operator(DeleteEmptyDeformGroups.bl_idname)
|
||||
layout.operator(DeleteUnselectedDeformGroups.bl_idname)
|
||||
layout.operator(DeleteUnusedVertexGroups.bl_idname)
|
||||
|
||||
class MESH_MT_vertex_group_mirror(bpy.types.Menu):
|
||||
bl_label = "Mirror"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.operator("object.vertex_group_mirror", icon='ARROW_LEFTRIGHT').use_topology = False
|
||||
layout.operator("object.vertex_group_mirror", text="Mirror Vertex Group (Topology)").use_topology = True
|
||||
|
||||
class MESH_MT_vertex_group_sort(bpy.types.Menu):
|
||||
bl_label = "Sort"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.operator(
|
||||
"object.vertex_group_sort",
|
||||
icon='SORTALPHA',
|
||||
text="Sort by Name",
|
||||
).sort_type = 'NAME'
|
||||
layout.operator(
|
||||
"object.vertex_group_sort",
|
||||
icon='BONE_DATA',
|
||||
text="Sort by Bone Hierarchy",
|
||||
).sort_type = 'BONE_HIERARCHY'
|
||||
|
||||
class MESH_MT_vertex_group_copy(bpy.types.Menu):
|
||||
bl_label = "Copy"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
|
||||
layout.operator("object.vertex_group_copy", icon='DUPLICATE')
|
||||
layout.operator("object.vertex_group_copy_to_linked")
|
||||
layout.operator("object.vertex_group_copy_to_selected")
|
||||
|
||||
class MESH_MT_vertex_group_lock(bpy.types.Menu):
|
||||
bl_label = "Batch Lock"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
|
||||
props = layout.operator("object.vertex_group_lock", icon='LOCKED', text="Lock All")
|
||||
props.action, props.mask = 'LOCK', 'ALL'
|
||||
props = layout.operator("object.vertex_group_lock", icon='UNLOCKED', text="Unlock All")
|
||||
props.action, props.mask = 'UNLOCK', 'ALL'
|
||||
props = layout.operator("object.vertex_group_lock", text="Invert All Locks")
|
||||
props.action, props.mask = 'INVERT', 'ALL'
|
||||
|
||||
class MESH_MT_vertex_group_weight(bpy.types.Menu):
|
||||
bl_label = "Weights"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
|
||||
layout.operator(
|
||||
"object.vertex_group_remove_from",
|
||||
icon='X',
|
||||
text="Remove Selected Verts from All Groups",
|
||||
).use_all_groups = True
|
||||
layout.operator("object.vertex_group_clean", text="Clean 0 Weights from All Groups").group_select_mode = 'ALL'
|
||||
layout.separator()
|
||||
layout.operator("object.vertex_group_remove_from", text="Remove All Verts from Selected Group").use_all_verts = True
|
||||
|
||||
def draw_misc(self, context):
|
||||
layout = self.layout
|
||||
layout.operator(FocusDeformBones.bl_idname)
|
||||
layout.operator(CreateMirrorGroups.bl_idname)
|
||||
|
||||
def draw_vertex_group_menu(self, context):
|
||||
layout = self.layout
|
||||
layout.row().menu(menu='MESH_MT_vertex_group_batch_delete')
|
||||
layout.row().menu(menu='MESH_MT_vertex_group_mirror')
|
||||
layout.row().menu(menu='MESH_MT_vertex_group_sort')
|
||||
layout.row().menu(menu='MESH_MT_vertex_group_copy')
|
||||
layout.row().menu(menu='MESH_MT_vertex_group_lock')
|
||||
layout.row().menu(menu='MESH_MT_vertex_group_weight')
|
||||
|
||||
classes = [
|
||||
MESH_MT_vertex_group_batch_delete,
|
||||
MESH_MT_vertex_group_mirror,
|
||||
MESH_MT_vertex_group_sort,
|
||||
MESH_MT_vertex_group_copy,
|
||||
MESH_MT_vertex_group_lock,
|
||||
MESH_MT_vertex_group_weight
|
||||
]
|
||||
|
||||
def register():
|
||||
from bpy.utils import register_class
|
||||
for c in classes:
|
||||
register_class(c)
|
||||
|
||||
|
||||
bpy.types.MESH_MT_vertex_group_context_menu.old_draw = bpy.types.MESH_MT_vertex_group_context_menu.draw
|
||||
bpy.types.MESH_MT_vertex_group_context_menu.remove(bpy.types.MESH_MT_vertex_group_context_menu.draw)
|
||||
|
||||
bpy.types.MESH_MT_vertex_group_context_menu.append(draw_vertex_group_menu)
|
||||
bpy.types.MESH_MT_vertex_group_context_menu.append(draw_misc)
|
||||
|
||||
def unregister():
|
||||
from bpy.utils import unregister_class
|
||||
|
||||
bpy.types.MESH_MT_vertex_group_context_menu.draw = bpy.types.MESH_MT_vertex_group_context_menu.old_draw
|
||||
del bpy.types.MESH_MT_vertex_group_context_menu.old_draw
|
||||
bpy.types.MESH_MT_vertex_group_context_menu.remove(draw_vertex_group_menu)
|
||||
bpy.types.MESH_MT_vertex_group_context_menu.remove(draw_misc)
|
||||
|
||||
for c in classes:
|
||||
unregister_class(c)
|
238
vertex_group_operators.py
Normal file
238
vertex_group_operators.py
Normal file
@ -0,0 +1,238 @@
|
||||
|
||||
import bpy
|
||||
from typing import List
|
||||
from .utils.naming import flip_name
|
||||
|
||||
|
||||
def get_deforming_armature(mesh_ob) -> bpy.types.Object:
|
||||
for m in mesh_ob.modifiers:
|
||||
if m.type=='ARMATURE':
|
||||
return m.object
|
||||
|
||||
def delete_vgroups(mesh_ob, vgroups):
|
||||
for vg in vgroups:
|
||||
mesh_ob.vertex_groups.remove(vg)
|
||||
|
||||
|
||||
def get_deforming_vgroups(mesh_ob) -> List[bpy.types.VertexGroup]:
|
||||
arm_ob = get_deforming_armature(mesh_ob)
|
||||
all_vgroups = mesh_ob.vertex_groups
|
||||
deforming_vgroups = []
|
||||
for b in arm_ob.data.bones:
|
||||
if b.name in all_vgroups and b.use_deform:
|
||||
deforming_vgroups.append(all_vgroups[b.name])
|
||||
return deforming_vgroups
|
||||
|
||||
def get_empty_deforming_vgroups(mesh_ob) -> List[bpy.types.VertexGroup]:
|
||||
deforming_vgroups = get_deforming_vgroups(mesh_ob)
|
||||
empty_deforming_groups = [vg for vg in deforming_vgroups if not vgroup_has_weight(mesh_ob, vg)]
|
||||
return empty_deforming_groups
|
||||
|
||||
def get_non_deforming_vgroups(mesh_ob) -> set:
|
||||
all_vgroups = mesh_ob.vertex_groups
|
||||
deforming_vgroups = get_deforming_vgroups(mesh_ob)
|
||||
return set(all_vgroups) - set(deforming_vgroups)
|
||||
|
||||
def get_vgroup_weight_on_vert(vgroup, vert_idx) -> float:
|
||||
# Despite how terrible this is, as of 04/Jun/2021 it seems to be the
|
||||
# only only way to ask Blender if a vertex is assigned to a vertex group.
|
||||
try:
|
||||
w = vgroup.weight(vert_idx)
|
||||
if w > 0:
|
||||
return w
|
||||
except RuntimeError:
|
||||
return -1
|
||||
|
||||
def vgroup_has_weight(mesh_ob, vgroup) -> bool:
|
||||
for i in range(0, len(mesh_ob.data.vertices)):
|
||||
if get_vgroup_weight_on_vert(vgroup, i) > 0:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class DeleteEmptyDeformGroups(bpy.types.Operator):
|
||||
"""Delete vertex groups which are associated to deforming bones but don't have any weights."""
|
||||
bl_idname = "object.delete_empty_deform_vgroups"
|
||||
bl_label = "Delete Empty Deform Groups"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
obj = context.object
|
||||
ob_is_mesh = obj and obj.type=='MESH'
|
||||
ob_has_arm_mod = 'ARMATURE' in (m.type for m in obj.modifiers)
|
||||
return all((ob_is_mesh, obj.vertex_groups, ob_has_arm_mod))
|
||||
|
||||
def execute(self, context):
|
||||
delete_vgroups(context.object, get_empty_deforming_vgroups(context.object))
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class WeightPaintOperator(bpy.types.Operator):
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
obj = context.object
|
||||
rig = context.pose_object
|
||||
return all((context.mode=='PAINT_WEIGHT', obj, rig, obj.vertex_groups))
|
||||
|
||||
class DeleteUnselectedDeformGroups(WeightPaintOperator):
|
||||
"""Delete deforming vertex groups that do not belong to any of the selected pose bones."""
|
||||
bl_idname = "object.delete_unselected_deform_vgroups"
|
||||
bl_label = "Delete Unselected Deform Groups"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
def execute(self, context):
|
||||
deforming_groups = get_deforming_vgroups(context.object)
|
||||
unselected_def_groups = [vg for vg in deforming_groups if vg.name not in context.selected_pose_bones]
|
||||
delete_vgroups(context.object, unselected_def_groups)
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
def reveal_bone(bone, select=True):
|
||||
"""bone can be edit/pose/data bone.
|
||||
This function should work regardless of selection or visibility states"""
|
||||
if type(bone)==bpy.types.PoseBone:
|
||||
bone = bone.bone
|
||||
armature = bone.id_data
|
||||
enabled_layers = [i for i in range(32) if armature.layers[i]]
|
||||
|
||||
# If none of this bone's layers are enabled, enable the first one.
|
||||
bone_layers = [i for i in range(32) if bone.layers[i]]
|
||||
if not any([i in enabled_layers for i in bone_layers]):
|
||||
armature.layers[bone_layers[0]] = True
|
||||
|
||||
bone.hide = False
|
||||
|
||||
if select:
|
||||
bone.select = True
|
||||
|
||||
class FocusDeformBones(WeightPaintOperator):
|
||||
"""Reveal the layers of, unhide, and select the bones of all deforming vertex groups."""
|
||||
bl_idname = "object.focus_deform_vgroups"
|
||||
bl_label = "Focus Deform Groups"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
def execute(self, context):
|
||||
deform_groups = get_deforming_vgroups(context.object)
|
||||
rig = context.pose_object
|
||||
|
||||
# Deselect all bones
|
||||
for pb in context.selected_pose_bones[:]:
|
||||
pb.bone.select = False
|
||||
|
||||
# Reveal and select all deforming pose bones.
|
||||
for vg in deform_groups:
|
||||
pb = rig.pose.bones.get(vg.name)
|
||||
if not pb: continue
|
||||
reveal_bone(pb.bone)
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
def get_referenced_vgroups(mesh_ob: bpy.types.Object, py_ob: object) -> List[bpy.types.VertexGroup]:
|
||||
"""Return a list of vertex groups directly referenced by the object's attributes."""
|
||||
referenced_vgroups = []
|
||||
for member in dir(py_ob):
|
||||
value = getattr(py_ob, member)
|
||||
if type(value) != str:
|
||||
continue
|
||||
vg = mesh_ob.vertex_groups.get(value)
|
||||
if vg:
|
||||
referenced_vgroups.append(vg.name)
|
||||
return referenced_vgroups
|
||||
|
||||
def get_shape_key_mask_vgroups(mesh_ob) -> List[bpy.types.VertexGroup]:
|
||||
mask_vgroups = []
|
||||
if not mesh_ob.data.shape_keys:
|
||||
return mask_vgroups
|
||||
for sk in mesh_ob.data.shape_keys.key_blocks:
|
||||
vg = mesh_ob.vertex_groups.get(sk.vertex_group)
|
||||
if vg and vg.name not in mask_vgroups:
|
||||
mask_vgroups.append(vg)
|
||||
|
||||
def delete_unused_vgroups(mesh_ob):
|
||||
non_deform_vgroups = get_non_deforming_vgroups(mesh_ob)
|
||||
used_vgroups = []
|
||||
|
||||
# Modifiers
|
||||
for m in mesh_ob.modifiers:
|
||||
used_vgroups.extend(get_referenced_vgroups(mesh_ob, m))
|
||||
# Physics settings
|
||||
if hasattr(m, 'settings'):
|
||||
used_vgroups.extend(get_referenced_vgroups(mesh_ob, m.settings))
|
||||
|
||||
# Shape Keys
|
||||
used_vgroups.extend(get_shape_key_mask_vgroups(mesh_ob))
|
||||
|
||||
# Constraints: TODO. This is a pretty rare case, and will require checking through the entire blend file.
|
||||
|
||||
groups_to_delete = set(non_deform_vgroups) - set(used_vgroups)
|
||||
delete_vgroups(mesh_ob, groups_to_delete)
|
||||
|
||||
|
||||
class DeleteUnusedVertexGroups(bpy.types.Operator):
|
||||
"""Delete non-deforming vertex groups which are not used by any modifiers, shape keys or constraints."""
|
||||
bl_idname = "object.delete_unused_vgroups"
|
||||
bl_label = "Delete Unused Groups"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
obj = context.object
|
||||
ob_is_mesh = obj and obj.type=='MESH'
|
||||
ob_has_groups = len(obj.vertex_groups) > 0
|
||||
return all((ob_is_mesh, ob_has_groups))
|
||||
|
||||
def execute(self, context):
|
||||
delete_unused_vgroups(context.object)
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class CreateMirrorGroups(bpy.types.Operator):
|
||||
"""Create missing Left/Right vertex groups to ensure correct behaviour of Mirror modifier."""
|
||||
bl_idname = "object.ensure_mirror_vgroups"
|
||||
bl_label = "Ensure Mirror Groups"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
obj = context.object
|
||||
ob_is_mesh = obj and obj.type=='MESH'
|
||||
ob_has_arm_mod = 'ARMATURE' in (m.type for m in obj.modifiers)
|
||||
ob_has_mirror_mod = 'MIRROR' in (m.type for m in obj.modifiers)
|
||||
return all((ob_is_mesh, obj.vertex_groups, ob_has_arm_mod, ob_has_mirror_mod))
|
||||
|
||||
def execute(self, context):
|
||||
obj = context.object
|
||||
deforming_groups = get_deforming_vgroups(obj)
|
||||
for vg in deforming_groups:
|
||||
flipped_name = flip_name(vg.name)
|
||||
if flipped_name == vg.name:
|
||||
continue
|
||||
obj.vertex_groups.new(name=flipped_name)
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
classes = [
|
||||
DeleteEmptyDeformGroups,
|
||||
FocusDeformBones,
|
||||
DeleteUnselectedDeformGroups,
|
||||
DeleteUnusedVertexGroups,
|
||||
CreateMirrorGroups,
|
||||
]
|
||||
|
||||
|
||||
def register():
|
||||
from bpy.utils import register_class
|
||||
for c in classes:
|
||||
register_class(c)
|
||||
|
||||
|
||||
def unregister():
|
||||
from bpy.utils import unregister_class
|
||||
for c in classes:
|
||||
try:
|
||||
unregister_class(c)
|
||||
except RuntimeError:
|
||||
pass # sometimes fails to unregister for literally no reason.
|
Loading…
Reference in New Issue
Block a user