Merge branch 'Current-Dev' into patch-1

This commit is contained in:
Onan Chew
2025-03-31 18:36:53 -04:00
committed by GitHub
49 changed files with 4763 additions and 2986 deletions
+89 -115
View File
@@ -10,7 +10,7 @@ import numpy.typing as npt
from typing import Optional, Tuple, List, Set, Dict, Any, Generator, Callable, Union, Type
from mathutils import Vector, Matrix
from bpy.types import (Context, Object, Modifier, EditBone, Operator,
from bpy.types import (Context, Object, Modifier, EditBone, Operator, Material,
VertexGroup, ShapeKey, Bone, Mesh, Armature, PropertyGroup)
from functools import lru_cache
from bpy.props import PointerProperty, IntProperty, StringProperty
@@ -20,6 +20,47 @@ from ..core.translations import t
from ..core.dictionaries import bone_names
from .dictionaries import reverse_bone_lookup, bone_names
class SceneMatClass(PropertyGroup):
mat: PointerProperty(type=Material)
register_class(SceneMatClass)
class MaterialListBool:
#For the love that is holy do not ever touch these. If this was java I would make these private
#They should only be accessed via context.scene.texture_atlas_Has_Mat_List_Shown
#This is so we know if the materials are up to date. messing with these variables directly will make the thing blow up.
#The only exception to this is the ExpandSection_Materials operator which populates this with new data once the materials have changed and need reloading.
old_list: dict[str,list[Material]] = {}
bool_material_list_expand: dict[str,bool] = {}
def set_bool(self, value: bool) -> None:
MaterialListBool.bool_material_list_expand[bpy.context.scene.name] = value
if value == False:
MaterialListBool.old_list[bpy.context.scene.name] = []
def get_bool(self) -> bool:
newlist: list[Material] = []
for obj in bpy.context.scene.objects:
if len(obj.material_slots)>0:
for mat_slot in obj.material_slots:
if mat_slot.material:
if mat_slot.material not in newlist:
newlist.append(mat_slot.material)
still_the_same: bool = True
if bpy.context.scene.name in MaterialListBool.old_list:
for item in newlist:
if item not in MaterialListBool.old_list[bpy.context.scene.name]:
still_the_same = False
break
for item in MaterialListBool.old_list[bpy.context.scene.name]:
if item not in newlist:
still_the_same = False
break
else:
still_the_same = False
MaterialListBool.bool_material_list_expand[bpy.context.scene.name] = still_the_same
return MaterialListBool.bool_material_list_expand[bpy.context.scene.name]
class ProgressTracker:
"""Universal progress tracking for Avatar Toolkit operations"""
@@ -67,89 +108,6 @@ def get_armature_list(self: Optional[Any] = None, context: Optional[Context] = N
if not armatures:
return [('NONE', t("Armature.validation.no_armature"), '')]
return armatures
def validate_armature(armature: Object) -> Tuple[bool, List[str]]:
"""Enhanced armature validation with multiple validation modes"""
validation_mode = bpy.context.scene.avatar_toolkit.validation_mode
messages: List[str] = []
if validation_mode == 'NONE':
return True, []
if not armature or armature.type != 'ARMATURE' or not armature.data.bones:
return False, [t("Armature.validation.basic_check_failed")]
found_bones: Dict[str, Bone] = {bone.name.lower(): bone for bone in armature.data.bones}
essential_bones: Set[str] = {'hips', 'spine', 'chest', 'neck', 'head'}
missing_bones: List[str] = []
for bone in essential_bones:
if not any(alt_name in found_bones for alt_name in bone_names[bone]):
missing_bones.append(bone)
if missing_bones:
messages.append(t("Armature.validation.missing_bones", bones=", ".join(missing_bones)))
if validation_mode == 'STRICT':
hierarchy: List[Tuple[str, str]] = [
('hips', 'spine'), ('spine', 'chest'),
('chest', 'neck'), ('neck', 'head')
]
for parent, child in hierarchy:
if not validate_bone_hierarchy(found_bones, parent, child):
messages.append(t("Armature.validation.invalid_hierarchy",
parent=parent, child=child))
symmetry_pairs: List[Tuple[str, str, str]] = [('arm', 'l', 'r'), ('leg', 'l', 'r')]
for base, left, right in symmetry_pairs:
if not validate_symmetry(found_bones, base, left, right):
messages.append(t("Armature.validation.asymmetric_bones", bone=base))
if (not validate_symmetry(found_bones, 'hand', 'l', 'r') and
not validate_symmetry(found_bones, 'wrist', 'l', 'r')):
messages.append(t("Armature.validation.asymmetric_hand_wrist"))
is_valid: bool = len(messages) == 0
return is_valid, messages
def validate_bone_hierarchy(bones: Dict[str, Bone], parent_name: str, child_name: str) -> bool:
"""Validate if there is a valid parent-child relationship between bones"""
parent_bone: Optional[Bone] = None
child_bone: Optional[Bone] = None
for alt_name in bone_names[parent_name]:
if alt_name in bones:
parent_bone = bones[alt_name]
break
for alt_name in bone_names[child_name]:
if alt_name in bones:
child_bone = bones[alt_name]
break
if not parent_bone or not child_bone:
return False
return child_bone.parent == parent_bone
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"""
left_patterns: List[str] = [
f"{base}.{left}",
f"{base}_{left}",
f"{left}_{base}"
]
right_patterns: List[str] = [
f"{base}.{right}",
f"{base}_{right}",
f"{right}_{base}"
]
left_exists: bool = any(pattern in bones for pattern in left_patterns)
right_exists: bool = any(pattern in bones for pattern in right_patterns)
return left_exists and right_exists
def auto_select_single_armature(context: Context) -> None:
"""Automatically select armature if only one exists in scene"""
@@ -321,51 +279,67 @@ def validate_meshes(meshes: List[Object]) -> Tuple[bool, str]:
return False, t("Optimization.non_mesh_objects")
return True, ""
def join_mesh_objects(context: Context, meshes: List[Object], progress: Optional[ProgressTracker] = None) -> Optional[Object]:
"""Combines multiple mesh objects into a single mesh with proper cleanup and UV fixing"""
try:
# Store UV maps before joining
uv_maps_data = {}
for mesh in meshes:
uv_maps_data[mesh.name] = {uv.name: uv.data.copy() for uv in mesh.data.uv_layers}
def fast_uv_fix(obj: Object) -> None:
"""Fast UV coordinate fixing for joined meshes"""
if not obj or not obj.data or not obj.data.uv_layers:
return
current_mode = bpy.context.mode
if current_mode != 'EDIT_MESH':
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action='SELECT')
# Process all UV layers at once
bpy.ops.uv.select_all(action='SELECT')
bpy.ops.uv.pack_islands(margin=0.001)
if current_mode != 'EDIT_MESH':
bpy.ops.object.mode_set(mode=current_mode)
def join_mesh_objects(context: Context, meshes: List[Object], progress: Optional[ProgressTracker] = None) -> Optional[Object]:
"""Combines multiple mesh objects into a single mesh with optimized performance"""
try:
if not meshes:
return None
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.select_all(action='DESELECT')
for mesh in meshes:
# Create a list of valid meshes
valid_meshes = [mesh for mesh in meshes if mesh.name in bpy.data.objects]
if not valid_meshes:
return None
for mesh in valid_meshes:
mesh.select_set(True)
if context.selected_objects:
context.view_layer.objects.active = context.selected_objects[0]
context.view_layer.objects.active = valid_meshes[0]
if progress:
progress.step(t("Optimization.joining_meshes"))
if progress:
progress.step(t("Optimization.joining_meshes"))
bpy.ops.object.join()
bpy.ops.object.join()
joined_mesh = context.active_object
if progress:
progress.step(t("Optimization.applying_transforms"))
if progress:
progress.step(t("Optimization.applying_transforms"))
bpy.ops.object.transform_apply(location=True, rotation=True, scale=True)
bpy.ops.object.transform_apply(location=True, rotation=True, scale=True)
if progress:
progress.step(t("Optimization.fixing_uvs"))
if progress:
progress.step(t("Optimization.fixing_uvs"))
fix_uv_coordinates(context)
# Restore UV maps after joining
joined_mesh = context.active_object
for uv_name, uv_data in uv_maps_data.items():
for map_name, map_data in uv_data.items():
if map_name not in joined_mesh.data.uv_layers:
joined_mesh.data.uv_layers.new(name=map_name)
joined_mesh.data.uv_layers[map_name].data.foreach_set("uv", map_data)
return context.active_object
return None
fast_uv_fix(joined_mesh)
return joined_mesh
except Exception as e:
logger.error(f"Failed to join meshes: {str(e)}")
return None
def fix_uv_coordinates(context: Context) -> None:
"""Normalizes and fixes UV coordinates for the active mesh object"""
obj: Object = context.object