Add Easy_Weight to Addons #47

Merged
Nick Alberelli merged 48 commits from feature/easy_weights into main 2023-05-17 22:13:57 +02:00
5 changed files with 539 additions and 1 deletions
Showing only changes of commit 9aadee7a51 - Show all commits

View File

@ -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
View File

169
utils/naming.py Normal file
View 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
View 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
View 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.