Anim: add operator to remove unused bone collections

Add an operator to remove all unused bone collections. This is added to
the bone collections "specials" menu and the tree view context menu.

A bone collection is considered "unused" when it has no bones assigned,
and also no child collection that has bones assigned.
This commit is contained in:
Sybren A. Stüvel 2024-01-22 13:34:12 +01:00
parent 784ff4abc0
commit a7354fb2af
2 changed files with 90 additions and 0 deletions

View File

@ -588,6 +588,90 @@ class ARMATURE_OT_collection_show_all(Operator):
return {'FINISHED'}
class ARMATURE_OT_collection_remove_unused(Operator):
"""Remove all bone collections that have neither bones nor children."""
bl_idname = "armature.collection_remove_unused"
bl_label = "Remove Unused Bone Collections"
bl_description = ("Remove all bone collections that have neither bones nor children.\n"
"This is done recursively, so bone collections that only have unused children are also removed")
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
if not context.object or context.object.type != 'ARMATURE':
return False
arm = context.object.data
return len(arm.collections) > 0
def execute(self, context):
if context.object.mode == 'EDIT':
return self.execute_edit_mode(context)
armature = context.object.data
# Build a set of bone collections that don't contain any bones, and
# whose children also don't contain any bones.
bcolls_to_remove = {
bcoll
for bcoll in armature.collections_all
if len(bcoll.bones_recursive) == 0}
if not bcolls_to_remove:
self.report({'INFO'}, "All bone collections are in use")
return {'CANCELLED'}
self.remove_bcolls(armature, bcolls_to_remove)
return {'FINISHED'}
def execute_edit_mode(self, context):
# BoneCollection.bones_recursive or .bones are not available in armature
# edit mode, because that has a completely separate list of edit bones.
# This is why edit mode needs separate handling.
armature = context.object.data
bcolls_with_bones = {
bcoll
for ebone in armature.edit_bones
for bcoll in ebone.collections
}
bcolls_to_remove = []
for root in armature.collections:
self.visit(root, bcolls_with_bones, bcolls_to_remove)
if not bcolls_to_remove:
self.report({'INFO'}, "All bone collections are in use")
return {'CANCELLED'}
self.remove_bcolls(armature, bcolls_to_remove)
return {'FINISHED'}
def visit(self, bcoll, bcolls_with_bones, bcolls_to_remove):
has_bones = bcoll in bcolls_with_bones
for child in bcoll.children:
child_has_bones = self.visit(child, bcolls_with_bones, bcolls_to_remove)
has_bones = has_bones or child_has_bones
if not has_bones:
bcolls_to_remove.append(bcoll)
return has_bones
def remove_bcolls(self, armature, bcolls_to_remove):
# Count things before they get removed.
num_bcolls_before_removal = len(armature.collections_all)
num_bcolls_to_remove = len(bcolls_to_remove)
# Create a copy of bcolls_to_remove so that it doesn't change when we
# remove bone collections.
for bcoll in reversed(list(bcolls_to_remove)):
armature.collections.remove(bcoll)
self.report({'INFO'}, 'Removed %d of %d bone collections' %
(num_bcolls_to_remove, num_bcolls_before_removal))
classes = (
ANIM_OT_keying_set_export,
NLA_OT_bake,
@ -596,4 +680,5 @@ classes = (
ARMATURE_OT_copy_bone_color_to_selected,
ARMATURE_OT_collection_solo_visibility,
ARMATURE_OT_collection_show_all,
ARMATURE_OT_collection_remove_unused,
)

View File

@ -150,6 +150,10 @@ class ARMATURE_MT_collection_context_menu(Menu):
props.name = active_bcoll.name if active_bcoll else ""
layout.operator("armature.collection_show_all")
layout.separator()
layout.operator("armature.collection_remove_unused", text="Remove Unused")
class ARMATURE_MT_collection_tree_context_menu(Menu):
bl_label = "Bone Collections"
@ -167,6 +171,7 @@ class ARMATURE_MT_collection_tree_context_menu(Menu):
sub.enabled = not active_bcoll_is_locked
sub.operator("armature.collection_add", text="Add Child Collection")
sub.operator("armature.collection_remove")
sub.operator("armature.collection_remove_unused", text="Remove Unused Collections")
layout.separator()