Tools Panel Finished.

This commit is contained in:
Yusarina
2024-12-05 14:44:41 +00:00
parent 5ce3f9ff68
commit b1631b2868
6 changed files with 356 additions and 36 deletions
+75
View File
@@ -410,3 +410,78 @@ def restore_bone_transforms(bone: EditBone, transforms: Dict[str, Any]) -> None:
bone.tail = transforms['tail'] bone.tail = transforms['tail']
bone.roll = transforms['roll'] bone.roll = transforms['roll']
bone.matrix = transforms['matrix'] bone.matrix = transforms['matrix']
def get_vertex_weights(mesh_obj: Object, group_name: str) -> Dict[int, float]:
"""Get vertex weights for a specific vertex group"""
weights = {}
group_index = mesh_obj.vertex_groups[group_name].index
for vertex in mesh_obj.data.vertices:
for group in vertex.groups:
if group.group == group_index:
weights[vertex.index] = group.weight
return weights
def transfer_vertex_weights(mesh_obj: Object,
source_name: str,
target_name: str,
threshold: float = 0.01) -> None:
"""Transfer vertex weights from source to target group"""
if source_name not in mesh_obj.vertex_groups:
return
source_group = mesh_obj.vertex_groups[source_name]
target_group = mesh_obj.vertex_groups.get(target_name)
if not target_group:
target_group = mesh_obj.vertex_groups.new(name=target_name)
# Get source weights
weights = get_vertex_weights(mesh_obj, source_name)
# Transfer weights above threshold
for vertex_index, weight in weights.items():
if weight > threshold:
target_group.add([vertex_index], weight, 'ADD')
# Remove source group
mesh_obj.vertex_groups.remove(source_group)
def remove_unused_shapekeys(mesh_obj: Object, tolerance: float = 0.001) -> int:
"""Remove unused shape keys from a mesh object"""
if not mesh_obj.data.shape_keys:
return 0
key_blocks = mesh_obj.data.shape_keys.key_blocks
vertex_count = len(mesh_obj.data.vertices)
removed_count = 0
# Cache for relative key locations
cache = {}
locations = np.empty(3 * vertex_count, dtype=np.float32)
to_delete = []
for key in key_blocks:
if key == key.relative_key:
continue
# Get current key locations
key.data.foreach_get("co", locations)
# Get or calculate relative key locations
if key.relative_key.name not in cache:
rel_locations = np.empty(3 * vertex_count, dtype=np.float32)
key.relative_key.data.foreach_get("co", rel_locations)
cache[key.relative_key.name] = rel_locations
# Compare locations
locations -= cache[key.relative_key.name]
if (np.abs(locations) < tolerance).all():
if not any(c in key.name for c in "-=~"): # Skip category markers
to_delete.append(key.name)
# Remove marked shape keys
for key_name in to_delete:
mesh_obj.shape_key_remove(key_blocks[key_name])
removed_count += 1
return removed_count
+16
View File
@@ -100,6 +100,22 @@ class AvatarToolkitSceneProperties(PropertyGroup):
max=0.9999999 max=0.9999999
) )
connect_bones_min_distance: FloatProperty(
name=t("Tools.connect_bones_min_distance"),
description=t("Tools.connect_bones_min_distance_desc"),
default=0.005,
min=0.001,
max=0.1
)
merge_weights_threshold: FloatProperty(
name=t("Tools.merge_weights_threshold"),
description=t("Tools.merge_weights_threshold_desc"),
default=0.01,
min=0.0001,
max=1.0
)
def register() -> None: def register() -> None:
"""Register the Avatar Toolkit property group""" """Register the Avatar Toolkit property group"""
logger.info("Registering Avatar Toolkit properties") logger.info("Registering Avatar Toolkit properties")
+91
View File
@@ -0,0 +1,91 @@
import bpy
import numpy as np
from bpy.types import Operator, Context
from typing import Set
from ...core.translations import t
from ...core.logging_setup import logger
from ...core.common import get_active_armature, get_all_meshes, validate_armature, remove_unused_shapekeys
class AvatarToolkit_OT_ApplyTransforms(Operator):
"""Apply all transformations to armature and associated meshes"""
bl_idname = "avatar_toolkit.apply_transforms"
bl_label = t("Tools.apply_transforms")
bl_description = t("Tools.apply_transforms_desc")
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context: Context) -> bool:
armature = get_active_armature(context)
if not armature:
return False
is_valid, _ = validate_armature(armature)
return is_valid and context.mode == 'OBJECT'
def execute(self, context: Context) -> Set[str]:
try:
armature = get_active_armature(context)
logger.info(f"Applying transforms to {armature.name} and associated meshes")
# Select armature and meshes
bpy.ops.object.select_all(action='DESELECT')
armature.select_set(True)
context.view_layer.objects.active = armature
meshes = get_all_meshes(context)
for mesh in meshes:
mesh.select_set(True)
# Apply transforms
bpy.ops.object.transform_apply(location=True, rotation=True, scale=True)
self.report({'INFO'}, t("Tools.transforms_applied"))
return {'FINISHED'}
except Exception as e:
logger.error(f"Failed to apply transforms: {str(e)}")
self.report({'ERROR'}, str(e))
return {'CANCELLED'}
class AvatarToolkit_OT_CleanShapekeys(Operator):
"""Remove unused shape keys from meshes"""
bl_idname = "avatar_toolkit.clean_shapekeys"
bl_label = t("Tools.clean_shapekeys")
bl_description = t("Tools.clean_shapekeys_desc")
bl_options = {'REGISTER', 'UNDO'}
tolerance: bpy.props.FloatProperty(
name=t("Tools.shapekey_tolerance"),
description=t("Tools.shapekey_tolerance_desc"),
default=0.001,
min=0.0001,
max=0.1
)
@classmethod
def poll(cls, context: Context) -> bool:
armature = get_active_armature(context)
if not armature:
return False
is_valid, _ = validate_armature(armature)
return is_valid and context.mode == 'OBJECT' and len(get_all_meshes(context)) > 0
def execute(self, context: Context) -> Set[str]:
try:
logger.info("Starting shape key cleanup")
removed_count = 0
for mesh in get_all_meshes(context):
if not mesh.data.shape_keys or not mesh.data.shape_keys.use_relative:
continue
removed = remove_unused_shapekeys(mesh, self.tolerance)
removed_count += removed
logger.debug(f"Removed {removed} shape keys from {mesh.name}")
self.report({'INFO'}, t("Tools.shapekeys_removed", count=removed_count))
return {'FINISHED'}
except Exception as e:
logger.error(f"Failed to clean shape keys: {str(e)}")
self.report({'ERROR'}, str(e))
return {'CANCELLED'}
+161
View File
@@ -0,0 +1,161 @@
import bpy
import math
from typing import Set, List
from bpy.types import Operator, Context, Armature, EditBone
from ...core.translations import t
from ...core.logging_setup import logger
from ...core.common import get_active_armature, get_all_meshes, get_vertex_weights, transfer_vertex_weights, validate_armature
class AvatarToolkit_OT_ConnectBones(Operator):
"""Connect disconnected bones in chain"""
bl_idname = "avatar_toolkit.connect_bones"
bl_label = t("Tools.connect_bones")
bl_description = t("Tools.connect_bones_desc")
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context: Context) -> bool:
armature = get_active_armature(context)
if not armature:
return False
is_valid, _ = validate_armature(armature)
return is_valid
def execute(self, context: Context) -> Set[str]:
try:
armature = get_active_armature(context)
logger.info("Starting bone connection operation")
bpy.ops.object.mode_set(mode='EDIT')
edit_bones = armature.data.edit_bones
bones_connected = 0
min_distance = context.scene.avatar_toolkit.connect_bones_min_distance
excluded_bones = {'LeftEye', 'RightEye', 'Head', 'Hips'}
for bone in edit_bones:
if len(bone.children) == 1 and bone.name not in excluded_bones:
child = bone.children[0]
distance = math.dist(bone.tail, child.head)
if distance > min_distance:
logger.debug(f"Connecting bone {bone.name} to {child.name}")
bone.tail = child.head
if bone.parent and len(bone.parent.children) == 1:
bone.use_connect = True
bones_connected += 1
bpy.ops.object.mode_set(mode='OBJECT')
self.report({'INFO'}, t("Tools.connect_bones_success", count=bones_connected))
return {'FINISHED'}
except Exception as e:
logger.error(f"Failed to connect bones: {str(e)}")
self.report({'ERROR'}, str(e))
return {'CANCELLED'}
class AvatarToolkit_OT_MergeToActive(Operator):
"""Merge selected bones into active bone and transfer weights"""
bl_idname = "avatar_toolkit.merge_to_active"
bl_label = t("Tools.merge_to_active")
bl_description = t("Tools.merge_to_active_desc")
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context: Context) -> bool:
armature = get_active_armature(context)
if not armature:
return False
return context.mode == 'EDIT_ARMATURE' and context.active_bone
def execute(self, context: Context) -> Set[str]:
try:
armature = get_active_armature(context)
active_bone = context.active_bone
selected_bones = [b for b in context.selected_editable_bones if b != active_bone]
if not selected_bones:
self.report({'WARNING'}, t("Tools.no_bones_selected"))
return {'CANCELLED'}
logger.info(f"Merging {len(selected_bones)} bones into {active_bone.name}")
# Store weights before merging
meshes = get_all_meshes(context)
weight_data = {}
for bone in selected_bones:
for mesh in meshes:
if bone.name in mesh.vertex_groups:
weights = get_vertex_weights(mesh, bone.name)
weight_data.setdefault(mesh.name, {})[bone.name] = weights
# Transfer weights to active bone
threshold = context.scene.avatar_toolkit.merge_weights_threshold
for mesh_name, bone_weights in weight_data.items():
mesh = bpy.data.objects[mesh_name]
for bone_name, weights in bone_weights.items():
transfer_vertex_weights(mesh, bone_name, active_bone.name, threshold)
# Delete merged bones
for bone in selected_bones:
armature.data.edit_bones.remove(bone)
self.report({'INFO'}, t("Tools.merge_to_active_success", count=len(selected_bones)))
return {'FINISHED'}
except Exception as e:
logger.error(f"Failed to merge bones: {str(e)}")
self.report({'ERROR'}, str(e))
return {'CANCELLED'}
class AvatarToolkit_OT_MergeToParent(Operator):
"""Merge selected bones into their respective parents and transfer weights"""
bl_idname = "avatar_toolkit.merge_to_parent"
bl_label = t("Tools.merge_to_parent")
bl_description = t("Tools.merge_to_parent_desc")
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context: Context) -> bool:
armature = get_active_armature(context)
if not armature:
return False
return context.mode == 'EDIT_ARMATURE'
def execute(self, context: Context) -> Set[str]:
try:
armature = get_active_armature(context)
selected_bones = [b for b in context.selected_editable_bones if b.parent]
if not selected_bones:
self.report({'WARNING'}, t("Tools.no_bones_with_parent"))
return {'CANCELLED'}
logger.info(f"Merging {len(selected_bones)} bones to their parents")
# Store weights before merging
meshes = get_all_meshes(context)
merged_count = 0
threshold = context.scene.avatar_toolkit.merge_weights_threshold
for bone in selected_bones:
parent = bone.parent
if not parent:
continue
# Transfer weights to parent
for mesh in meshes:
if bone.name in mesh.vertex_groups:
transfer_vertex_weights(mesh, bone.name, parent.name, threshold)
# Delete merged bone
armature.data.edit_bones.remove(bone)
merged_count += 1
self.report({'INFO'}, t("Tools.merge_to_parent_success", count=merged_count))
return {'FINISHED'}
except Exception as e:
logger.error(f"Failed to merge bones: {str(e)}")
self.report({'ERROR'}, str(e))
return {'CANCELLED'}
+13
View File
@@ -174,6 +174,19 @@
"Tools.analyzing_weights": "Analyzing vertex weights...", "Tools.analyzing_weights": "Analyzing vertex weights...",
"Tools.removing_bones": "Removing unweighted bones...", "Tools.removing_bones": "Removing unweighted bones...",
"Tools.verifying_hierarchy": "Verifying bone hierarchy...", "Tools.verifying_hierarchy": "Verifying bone hierarchy...",
"Tools.connect_bones_min_distance": "Minimum Distance",
"Tools.connect_bones_min_distance_desc": "Minimum distance between bones to attempt connection",
"Tools.connect_bones_success": "Connected {count} bones",
"Tools.merge_weights_threshold": "Weight Transfer Threshold",
"Tools.merge_weights_threshold_desc": "Minimum weight value to transfer when merging bones",
"Tools.no_bones_selected": "No bones selected to merge",
"Tools.no_bones_with_parent": "No selected bones with parents found",
"Tools.merge_to_active_success": "Successfully merged {count} bones to active bone",
"Tools.merge_to_parent_success": "Successfully merged {count} bones to their parents",
"Tools.transforms_applied": "Transforms applied successfully",
"Tools.shapekey_tolerance": "Shape Key Tolerance",
"Tools.shapekey_tolerance_desc": "Minimum difference to consider a shape key as used",
"Tools.shapekeys_removed": "Removed {count} unused shape keys",
"Settings.label": "Settings", "Settings.label": "Settings",
"Settings.language": "Language", "Settings.language": "Language",
-36
View File
@@ -4,42 +4,6 @@ from bpy.types import Panel, Context, UILayout, Operator
from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME
from ..core.translations import t from ..core.translations import t
# Temporary Operator Classes for UI Preview
class AvatarToolkit_OT_MergeToActive(Operator):
bl_idname = "avatar_toolkit.merge_to_active"
bl_label = "Merge to Active"
def execute(self, context: Context) -> Set[str]:
return {'FINISHED'}
class AvatarToolkit_OT_MergeToParent(Operator):
bl_idname = "avatar_toolkit.merge_to_parent"
bl_label = "Merge to Parent"
def execute(self, context: Context) -> Set[str]:
return {'FINISHED'}
class AvatarToolkit_OT_ConnectBones(Operator):
bl_idname = "avatar_toolkit.connect_bones"
bl_label = "Connect Bones"
def execute(self, context: Context) -> Set[str]:
return {'FINISHED'}
class AvatarToolkit_OT_ApplyTransforms(Operator):
bl_idname = "avatar_toolkit.apply_transforms"
bl_label = "Apply Transforms"
def execute(self, context: Context) -> Set[str]:
return {'FINISHED'}
class AvatarToolkit_OT_CleanShapekeys(Operator):
bl_idname = "avatar_toolkit.clean_shapekeys"
bl_label = "Remove Unused Shapekeys"
def execute(self, context: Context) -> Set[str]:
return {'FINISHED'}
class AvatarToolKit_PT_ToolsPanel(Panel): class AvatarToolKit_PT_ToolsPanel(Panel):
"""Panel containing various tools for avatar customization and optimization""" """Panel containing various tools for avatar customization and optimization"""
bl_label: str = t("Tools.label") bl_label: str = t("Tools.label")