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
2 changed files with 108 additions and 37 deletions
Showing only changes of commit e7943d1fae - Show all commits

View File

@ -12,19 +12,39 @@ class MESH_MT_vertex_group_batch_delete(bpy.types.Menu):
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)
layout.operator(
"object.vertex_group_remove",
text="Delete All Groups",
icon='TRASH'
).all = True
layout.operator(
"object.vertex_group_remove",
text="Delete All Unlocked Groups",
icon='UNLOCKED'
).all_unlocked = True
layout.separator()
layout.operator(DeleteEmptyDeformGroups.bl_idname, icon='GROUP_BONE')
layout.operator(DeleteUnusedVertexGroups.bl_idname, icon='BRUSH_DATA')
layout.operator(DeleteUnselectedDeformGroups.bl_idname, icon='RESTRICT_SELECT_ON')
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
# TODO: Add an operator that duplicates and mirrors active group
# TODO: Add an operator that duplicates and mirrors groups of all selected pose bones, or simply all groups.
# All this functionality could be merged into a single operator with a pop-up that asks you if you want proximity or topology based mirroring for the active group, all groups, or selected pose bones.
layout.operator(
"object.vertex_group_mirror",
text="Mirror Active Group (Proximity)",
icon='AUTOMERGE_OFF'
).use_topology = False
layout.operator(
"object.vertex_group_mirror",
text="Mirror Active Group (Topology)",
icon='AUTOMERGE_ON'
).use_topology = True
class MESH_MT_vertex_group_sort(bpy.types.Menu):
bl_label = "Sort"
@ -34,12 +54,12 @@ class MESH_MT_vertex_group_sort(bpy.types.Menu):
layout.operator(
"object.vertex_group_sort",
icon='SORTALPHA',
text="Sort by Name",
text="By Name",
).sort_type = 'NAME'
layout.operator(
"object.vertex_group_sort",
icon='BONE_DATA',
text="Sort by Bone Hierarchy",
text="By Bone Hierarchy",
).sort_type = 'BONE_HIERARCHY'
class MESH_MT_vertex_group_copy(bpy.types.Menu):
@ -48,9 +68,12 @@ class MESH_MT_vertex_group_copy(bpy.types.Menu):
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")
# TODO: This isn't grayed out when there's no active group.
# TODO: Maybe for things that use the active group, we should put the name of the group in the button text? Makes it harder to search tho perhaps. Not even sure if menu search supports dynamic menu text?
layout.operator("object.vertex_group_copy", icon='DUPLICATE', text="Duplicate Group")
layout.separator()
layout.operator("object.vertex_group_copy_to_linked", text="Synchronize Groups on All Instances", icon='LINKED')
layout.operator("object.vertex_group_copy_to_selected", text="Synchronize Groups on Selected", icon = 'RESTRICT_SELECT_OFF')
class MESH_MT_vertex_group_lock(bpy.types.Menu):
bl_label = "Batch Lock"
@ -62,7 +85,7 @@ class MESH_MT_vertex_group_lock(bpy.types.Menu):
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 = layout.operator("object.vertex_group_lock", icon='UV_SYNC_SELECT', text="Invert All Locks")
props.action, props.mask = 'INVERT', 'ALL'
class MESH_MT_vertex_group_weight(bpy.types.Menu):
@ -73,26 +96,38 @@ class MESH_MT_vertex_group_weight(bpy.types.Menu):
layout.operator(
"object.vertex_group_remove_from",
icon='X',
icon='MESH_DATA',
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.operator(
"object.vertex_group_clean",
icon='BRUSH_DATA',
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
layout.operator(
"object.vertex_group_remove_from",
icon='TRASH',
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)
layout.operator(FocusDeformBones.bl_idname, icon='ZOOM_IN')
layout.operator(CreateMirrorGroups.bl_idname, icon='MOD_MIRROR')
# TODO: Add an operator called "Smart Cleanup" that creates missing mirror groups,
# Cleans 0 weights,
# Deletes unused deforming groups,
# and deletes unused non-deforming groups.
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')
layout.row().menu(menu='MESH_MT_vertex_group_batch_delete', icon='TRASH')
layout.row().menu(menu='MESH_MT_vertex_group_mirror', icon='ARROW_LEFTRIGHT')
layout.row().menu(menu='MESH_MT_vertex_group_sort', icon='SORTALPHA')
layout.row().menu(menu='MESH_MT_vertex_group_copy', icon='DUPLICATE')
layout.row().menu(menu='MESH_MT_vertex_group_lock', icon='LOCKED')
layout.row().menu(menu='MESH_MT_vertex_group_weight', icon='MOD_VERTEX_WEIGHT')
classes = [
MESH_MT_vertex_group_batch_delete,

View File

@ -26,6 +26,19 @@ def get_deforming_vgroups(mesh_ob) -> List[bpy.types.VertexGroup]:
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)]
# Always account for Mirror modifier:
if not 'MIRROR' in [m.type for m in mesh_ob.modifiers]:
return empty_deforming_groups
# A group is not considered empty if it is the opposite of a non-empty group.
for empty_vg in empty_deforming_groups[:]:
opposite_vg = mesh_ob.vertex_groups.get(flip_name(empty_vg.name))
if not opposite_vg:
continue
if opposite_vg not in empty_deforming_groups:
empty_deforming_groups.remove(empty_vg)
return empty_deforming_groups
def get_non_deforming_vgroups(mesh_ob) -> set:
@ -38,8 +51,7 @@ def get_vgroup_weight_on_vert(vgroup, vert_idx) -> float:
# 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
return w
except RuntimeError:
return -1
@ -51,7 +63,7 @@ def vgroup_has_weight(mesh_ob, vgroup) -> bool:
class DeleteEmptyDeformGroups(bpy.types.Operator):
"""Delete vertex groups which are associated to deforming bones but don't have any weights."""
"""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'}
@ -64,7 +76,12 @@ class DeleteEmptyDeformGroups(bpy.types.Operator):
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))
empty_groups = get_empty_deforming_vgroups(context.object)
num_groups = len(empty_groups)
print(f"Deleting empty deform groups:")
print(" " + "\n ".join([vg.name for vg in empty_groups]))
self.report({'INFO'}, f"Deleted {num_groups} empty deform groups.")
delete_vgroups(context.object, empty_groups)
return {'FINISHED'}
@ -76,15 +93,21 @@ class WeightPaintOperator(bpy.types.Operator):
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."""
"""Delete deforming vertex groups that do not correspond to any selected pose bone"""
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]
selected_bone_names = [b.name for b in context.selected_pose_bones]
unselected_def_groups = [vg for vg in deforming_groups if vg.name not in selected_bone_names]
print(f"Deleting unselected deform groups:")
deleted_names = [vg.name for vg in unselected_def_groups]
print(" " + "\n ".join(deleted_names))
delete_vgroups(context.object, unselected_def_groups)
self.report({'INFO'}, f"Deleted {len(deleted_names)} unselected deform groups.")
return {'FINISHED'}
@ -107,7 +130,7 @@ def reveal_bone(bone, select=True):
bone.select = True
class FocusDeformBones(WeightPaintOperator):
"""Reveal the layers of, unhide, and select the bones of all deforming vertex groups."""
"""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'}
@ -150,7 +173,7 @@ def get_shape_key_mask_vgroups(mesh_ob) -> List[bpy.types.VertexGroup]:
if vg and vg.name not in mask_vgroups:
mask_vgroups.append(vg)
def delete_unused_vgroups(mesh_ob):
def delete_unused_vgroups(mesh_ob) -> List[str]:
non_deform_vgroups = get_non_deforming_vgroups(mesh_ob)
used_vgroups = []
@ -167,13 +190,16 @@ def delete_unused_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)
names = [vg.name for vg in groups_to_delete]
print(f"Deleting unused non-deform groups:")
print(" " + "\n ".join(names))
delete_vgroups(mesh_ob, groups_to_delete)
return names
class DeleteUnusedVertexGroups(bpy.types.Operator):
"""Delete non-deforming vertex groups which are not used by any modifiers, shape keys or constraints."""
"""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_label = "Delete Unused Non-Deform Groups"
bl_options = {'REGISTER', 'UNDO'}
@classmethod
@ -184,12 +210,14 @@ class DeleteUnusedVertexGroups(bpy.types.Operator):
return all((ob_is_mesh, ob_has_groups))
def execute(self, context):
delete_unused_vgroups(context.object)
deleted_names = delete_unused_vgroups(context.object)
self.report({'INFO'}, f"Deleted {len(deleted_names)} unused non-deform groups.")
return {'FINISHED'}
class CreateMirrorGroups(bpy.types.Operator):
"""Create missing Left/Right vertex groups to ensure correct behaviour of Mirror modifier."""
"""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'}
@ -205,11 +233,19 @@ class CreateMirrorGroups(bpy.types.Operator):
def execute(self, context):
obj = context.object
deforming_groups = get_deforming_vgroups(obj)
new_counter = 0
print("Creating missing Mirror groups:")
for vg in deforming_groups:
flipped_name = flip_name(vg.name)
if flipped_name == vg.name:
continue
if flipped_name in obj.vertex_groups:
continue
obj.vertex_groups.new(name=flipped_name)
print(" "+flipped_name)
new_counter += 1
self.report({'INFO'}, f"Created {new_counter} missing groups")
return {'FINISHED'}