Add Easy_Weight
to Addons
#47
@ -32,6 +32,8 @@ from . import force_apply_mirror
|
|||||||
from . import toggle_weight_paint
|
from . import toggle_weight_paint
|
||||||
from . import change_brush
|
from . import change_brush
|
||||||
from . import weight_paint_context_menu
|
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.
|
# Each module is expected to have a register() and unregister() function.
|
||||||
modules = [
|
modules = [
|
||||||
@ -39,7 +41,9 @@ modules = [
|
|||||||
force_apply_mirror,
|
force_apply_mirror,
|
||||||
toggle_weight_paint,
|
toggle_weight_paint,
|
||||||
change_brush,
|
change_brush,
|
||||||
weight_paint_context_menu
|
weight_paint_context_menu,
|
||||||
|
vertex_group_operators,
|
||||||
|
vertex_group_menu
|
||||||
]
|
]
|
||||||
|
|
||||||
class EasyWeightPreferences(AddonPreferences):
|
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