Merge pull request #172 from Yusarina/Current
Fixes asymmetric being incorrectly detected #169
This commit is contained in:
+155
-1
@@ -211,9 +211,163 @@ def validate_bone_hierarchy(bones: Dict[str, Bone], parent_name: str, child_name
|
|||||||
return False
|
return False
|
||||||
return bones[child_name].parent == bones[parent_name]
|
return bones[child_name].parent == bones[parent_name]
|
||||||
|
|
||||||
|
def extract_bone_side_info(bone_name: str) -> Tuple[str, str]:
|
||||||
|
"""
|
||||||
|
Extract base bone name and side indicator from a bone name.
|
||||||
|
Returns (base_name, side) where side is 'L', 'R', or ''
|
||||||
|
"""
|
||||||
|
normalized = simplify_bonename(bone_name)
|
||||||
|
original = bone_name
|
||||||
|
|
||||||
|
# Common left/right patterns to check
|
||||||
|
left_patterns = [
|
||||||
|
'left', 'l', 'lft', 'lt',
|
||||||
|
'.l', '_l', '-l', ' l',
|
||||||
|
'左', 'ひだり'
|
||||||
|
]
|
||||||
|
|
||||||
|
right_patterns = [
|
||||||
|
'right', 'r', 'rgt', 'rt',
|
||||||
|
'.r', '_r', '-r', ' r',
|
||||||
|
'右', 'みぎ'
|
||||||
|
]
|
||||||
|
|
||||||
|
# Check for left patterns
|
||||||
|
for pattern in left_patterns:
|
||||||
|
pattern_norm = simplify_bonename(pattern)
|
||||||
|
if normalized.startswith(pattern_norm):
|
||||||
|
base = normalized[len(pattern_norm):]
|
||||||
|
if base: # Make sure there's something left
|
||||||
|
return base, 'L'
|
||||||
|
elif normalized.endswith(pattern_norm):
|
||||||
|
base = normalized[:-len(pattern_norm)]
|
||||||
|
if base:
|
||||||
|
return base, 'L'
|
||||||
|
elif pattern_norm in normalized:
|
||||||
|
# Handle cases like ArmLeft
|
||||||
|
parts = normalized.split(pattern_norm)
|
||||||
|
if len(parts) == 2:
|
||||||
|
base = parts[0] + parts[1]
|
||||||
|
if base:
|
||||||
|
return base, 'L'
|
||||||
|
|
||||||
|
# Check for right patterns
|
||||||
|
for pattern in right_patterns:
|
||||||
|
pattern_norm = simplify_bonename(pattern)
|
||||||
|
if normalized.startswith(pattern_norm):
|
||||||
|
base = normalized[len(pattern_norm):]
|
||||||
|
if base:
|
||||||
|
return base, 'R'
|
||||||
|
elif normalized.endswith(pattern_norm):
|
||||||
|
base = normalized[:-len(pattern_norm)]
|
||||||
|
if base:
|
||||||
|
return base, 'R'
|
||||||
|
elif pattern_norm in normalized:
|
||||||
|
parts = normalized.split(pattern_norm)
|
||||||
|
if len(parts) == 2:
|
||||||
|
base = parts[0] + parts[1]
|
||||||
|
if base:
|
||||||
|
return base, 'R'
|
||||||
|
|
||||||
|
return normalized, ''
|
||||||
|
|
||||||
|
def find_symmetric_bone_pairs(bones: Dict[str, Bone]) -> Dict[str, Tuple[List[str], List[str]]]:
|
||||||
|
"""
|
||||||
|
Automatically find symmetric bone pairs in the armature.
|
||||||
|
Returns dict mapping base_name to (left_bones, right_bones)
|
||||||
|
"""
|
||||||
|
bone_groups = {}
|
||||||
|
|
||||||
|
for bone_name in bones.keys():
|
||||||
|
base, side = extract_bone_side_info(bone_name)
|
||||||
|
|
||||||
|
if side:
|
||||||
|
if base not in bone_groups:
|
||||||
|
bone_groups[base] = {'L': [], 'R': []}
|
||||||
|
bone_groups[base][side].append(bone_name)
|
||||||
|
|
||||||
|
symmetric_pairs = {}
|
||||||
|
for base, sides in bone_groups.items():
|
||||||
|
if sides['L'] and sides['R']:
|
||||||
|
symmetric_pairs[base] = (sides['L'], sides['R'])
|
||||||
|
|
||||||
|
return symmetric_pairs
|
||||||
|
|
||||||
|
def validate_armature_symmetry(armature: Object) -> Tuple[bool, List[str]]:
|
||||||
|
"""
|
||||||
|
Comprehensive symmetry validation that provides detailed feedback
|
||||||
|
"""
|
||||||
|
if not armature or armature.type != 'ARMATURE':
|
||||||
|
return False, ["Invalid armature"]
|
||||||
|
|
||||||
|
bones = {bone.name: bone for bone in armature.data.bones}
|
||||||
|
symmetric_pairs = find_symmetric_bone_pairs(bones)
|
||||||
|
|
||||||
|
messages = []
|
||||||
|
is_symmetric = True
|
||||||
|
|
||||||
|
if symmetric_pairs:
|
||||||
|
messages.append("Found symmetric bone pairs:")
|
||||||
|
for base, (left_bones, right_bones) in symmetric_pairs.items():
|
||||||
|
left_count = len(left_bones)
|
||||||
|
right_count = len(right_bones)
|
||||||
|
|
||||||
|
if left_count == right_count:
|
||||||
|
messages.append(f" ✓ {base}: {left_count} bones on each side")
|
||||||
|
for l_bone, r_bone in zip(sorted(left_bones), sorted(right_bones)):
|
||||||
|
messages.append(f" {l_bone} ↔ {r_bone}")
|
||||||
|
else:
|
||||||
|
is_symmetric = False
|
||||||
|
messages.append(f" ✗ {base}: {left_count} left, {right_count} right bones")
|
||||||
|
messages.append(f" Left: {', '.join(sorted(left_bones))}")
|
||||||
|
messages.append(f" Right: {', '.join(sorted(right_bones))}")
|
||||||
|
else:
|
||||||
|
messages.append("No symmetric bone pairs detected")
|
||||||
|
is_symmetric = False
|
||||||
|
|
||||||
|
return is_symmetric, messages
|
||||||
|
|
||||||
def validate_symmetry(bones: Dict[str, Bone], base: str, left: str, right: str) -> bool:
|
def validate_symmetry(bones: Dict[str, Bone], base: str, left: str, right: str) -> bool:
|
||||||
"""Validate if matching left and right bones exist for a given base bone name"""
|
"""Validate if matching left and right bones exist for a given base bone name"""
|
||||||
# Extract left and right bone names from both hierarchies
|
# First try the new intelligent detection
|
||||||
|
symmetric_pairs = find_symmetric_bone_pairs(bones)
|
||||||
|
|
||||||
|
# Look for bones that match the requested base type
|
||||||
|
matching_left_bones = []
|
||||||
|
matching_right_bones = []
|
||||||
|
|
||||||
|
# Check each detected symmetric pair
|
||||||
|
for pair_base, (left_bones, right_bones) in symmetric_pairs.items():
|
||||||
|
if base.lower() in pair_base.lower() or pair_base.lower() in base.lower():
|
||||||
|
matching_left_bones.extend(left_bones)
|
||||||
|
matching_right_bones.extend(right_bones)
|
||||||
|
|
||||||
|
if matching_left_bones or matching_right_bones:
|
||||||
|
left_bases = {}
|
||||||
|
right_bases = {}
|
||||||
|
|
||||||
|
for bone_name in matching_left_bones:
|
||||||
|
bone_base, side = extract_bone_side_info(bone_name)
|
||||||
|
if bone_base not in left_bases:
|
||||||
|
left_bases[bone_base] = []
|
||||||
|
left_bases[bone_base].append(bone_name)
|
||||||
|
|
||||||
|
for bone_name in matching_right_bones:
|
||||||
|
bone_base, side = extract_bone_side_info(bone_name)
|
||||||
|
if bone_base not in right_bases:
|
||||||
|
right_bases[bone_base] = []
|
||||||
|
right_bases[bone_base].append(bone_name)
|
||||||
|
|
||||||
|
all_bases = set(left_bases.keys()) | set(right_bases.keys())
|
||||||
|
for bone_base in all_bases:
|
||||||
|
left_count = len(left_bases.get(bone_base, []))
|
||||||
|
right_count = len(right_bases.get(bone_base, []))
|
||||||
|
if left_count != right_count:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return len(all_bases) > 0
|
||||||
|
|
||||||
|
# Fallback to original dictionary-based method
|
||||||
left_bone_names = set()
|
left_bone_names = set()
|
||||||
right_bone_names = set()
|
right_bone_names = set()
|
||||||
|
|
||||||
|
|||||||
@@ -186,7 +186,6 @@ class AvatarToolKit_OT_RemoveZeroWeightBones(Operator):
|
|||||||
if not armature:
|
if not armature:
|
||||||
return {'CANCELLED'}
|
return {'CANCELLED'}
|
||||||
|
|
||||||
# Store initial transforms
|
|
||||||
bpy.ops.object.mode_set(mode='EDIT')
|
bpy.ops.object.mode_set(mode='EDIT')
|
||||||
initial_transforms: Dict[str, Dict[str, Any]] = {}
|
initial_transforms: Dict[str, Dict[str, Any]] = {}
|
||||||
data_breaking = store_breaking_settings_armature(armature)
|
data_breaking = store_breaking_settings_armature(armature)
|
||||||
@@ -200,56 +199,61 @@ class AvatarToolKit_OT_RemoveZeroWeightBones(Operator):
|
|||||||
'parent': bone.parent.name if bone.parent else None
|
'parent': bone.parent.name if bone.parent else None
|
||||||
}
|
}
|
||||||
|
|
||||||
# Get weighted bones
|
# Get bones with any weight
|
||||||
weighted_bones: List[str] = []
|
weighted_bones: List[str] = []
|
||||||
meshes = get_all_meshes(context)
|
meshes = get_all_meshes(context)
|
||||||
zero_weight_bones: List[str] = []
|
|
||||||
|
|
||||||
for mesh in meshes:
|
for mesh in meshes:
|
||||||
mesh_data: Mesh = mesh.data
|
for vertex in mesh.data.vertices:
|
||||||
for vertex in mesh_data.vertices:
|
|
||||||
for group in vertex.groups:
|
for group in vertex.groups:
|
||||||
if group.weight > context.scene.avatar_toolkit.merge_weights_threshold:
|
if group.weight > context.scene.avatar_toolkit.merge_weights_threshold:
|
||||||
weighted_bones.append(mesh.vertex_groups[group.group].name)
|
vg = mesh.vertex_groups[group.group]
|
||||||
|
if vg.name not in weighted_bones:
|
||||||
|
weighted_bones.append(vg.name)
|
||||||
|
|
||||||
# Process bone removal
|
armature_data = armature.data
|
||||||
bpy.ops.object.mode_set(mode='EDIT')
|
|
||||||
armature_data: Armature = armature.data
|
|
||||||
removed_count = 0
|
removed_count = 0
|
||||||
|
zero_weight_bones: List[str] = []
|
||||||
|
|
||||||
for bone in armature_data.edit_bones[:]: # Create a copy of the list
|
def is_zero_weight_chain(bone, weighted_bones, preserve_check_fn):
|
||||||
if (bone.name not in weighted_bones and
|
if bone.name in weighted_bones or preserve_check_fn(bone.name, context):
|
||||||
not self.should_preserve_bone(bone.name, context)):
|
return False
|
||||||
|
return all(is_zero_weight_chain(child, weighted_bones, preserve_check_fn) for child in bone.children)
|
||||||
|
|
||||||
|
for bone in armature_data.edit_bones[:]:
|
||||||
|
if bone.name in weighted_bones or self.should_preserve_bone(bone.name, context):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not is_zero_weight_chain(bone, weighted_bones, self.should_preserve_bone):
|
||||||
|
continue
|
||||||
|
|
||||||
if context.scene.avatar_toolkit.list_only_mode:
|
if context.scene.avatar_toolkit.list_only_mode:
|
||||||
zero_weight_bones.append(bone.name)
|
zero_weight_bones.append(bone.name)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Store children data
|
# Traverse and collect the full empty chain
|
||||||
children = bone.children
|
stack = [bone]
|
||||||
children_data = {child.name: initial_transforms[child.name] for child in children}
|
chain = []
|
||||||
|
|
||||||
# Reparent children
|
while stack:
|
||||||
for child in children:
|
b = stack.pop()
|
||||||
|
chain.append(b)
|
||||||
|
stack.extend(b.children)
|
||||||
|
|
||||||
|
for b in reversed(chain): # Remove children before parents
|
||||||
|
for child in b.children:
|
||||||
child.use_connect = False
|
child.use_connect = False
|
||||||
if bone.parent:
|
if b.parent:
|
||||||
child.parent = bone.parent
|
child.parent = b.parent
|
||||||
|
if b.name in armature_data.edit_bones:
|
||||||
# Remove bone
|
armature_data.edit_bones.remove(b)
|
||||||
armature_data.edit_bones.remove(bone)
|
|
||||||
removed_count += 1
|
removed_count += 1
|
||||||
|
|
||||||
# Restore children positions
|
|
||||||
for child_name, data in children_data.items():
|
|
||||||
if child_name in armature_data.edit_bones:
|
|
||||||
child = armature_data.edit_bones[child_name]
|
|
||||||
restore_bone_transforms(child, data)
|
|
||||||
|
|
||||||
bpy.ops.object.mode_set(mode='OBJECT')
|
bpy.ops.object.mode_set(mode='OBJECT')
|
||||||
|
|
||||||
if context.scene.avatar_toolkit.list_only_mode:
|
if context.scene.avatar_toolkit.list_only_mode:
|
||||||
self.populate_bone_list(context, zero_weight_bones)
|
self.populate_bone_list(context, zero_weight_bones)
|
||||||
return {'FINISHED'}
|
return {'FINISHED'}
|
||||||
|
|
||||||
restore_breaking_settings_armature(armature, data_breaking)
|
restore_breaking_settings_armature(armature, data_breaking)
|
||||||
self.report({'INFO'}, t("Tools.clean_weights_success", count=removed_count))
|
self.report({'INFO'}, t("Tools.clean_weights_success", count=removed_count))
|
||||||
return {'FINISHED'}
|
return {'FINISHED'}
|
||||||
|
|||||||
Reference in New Issue
Block a user