Fixes and Improvements

- Improved typing in some areas.
- Improved code readability in some areas.
- Delete bone constraints would error out if the user is in edit mode, we now start in Object mode first.
- Fixed Eye tracking Ajust string not being in the translation files.
- There is now a selection box to select the mesh in the current active armature for viseme creation instead of the user having to select it in the 3D scene.
- Viseme preview mode won't allow you to start it if your in a other mode, you need to be in Object mode now.
- Combine Materials won't allow you to start it if your in a other mode, you need to be in Object mode now.
- Added Japanese and Korean UI Languages.
This commit is contained in:
Yusarina
2024-12-18 02:44:26 +00:00
parent c5d07892c2
commit 8665292c7b
15 changed files with 1338 additions and 778 deletions
+155 -170
View File
@@ -6,10 +6,12 @@ import webbrowser
import typing
import struct
from io import BytesIO
import numpy.typing as npt
from typing import Optional, Tuple, List, Set, Dict, Any, Generator, Callable
from mathutils import Vector
from bpy.types import Context, Object, Modifier, EditBone, Operator
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,
VertexGroup, ShapeKey, Bone, Mesh, Armature, PropertyGroup)
from functools import lru_cache
from bpy.props import PointerProperty, IntProperty, StringProperty
from bpy.utils import register_class
@@ -20,11 +22,11 @@ from ..core.dictionaries import bone_names
class ProgressTracker:
"""Universal progress tracking for Avatar Toolkit operations"""
def __init__(self, context: Context, total_steps: int, operation_name: str = "Operation"):
self.context = context
self.total = total_steps
self.current = 0
self.operation_name = operation_name
def __init__(self, context: Context, total_steps: int, operation_name: str = "Operation") -> None:
self.context: Context = context
self.total: int = total_steps
self.current: int = 0
self.operation_name: str = operation_name
self.wm = context.window_manager
def step(self, message: str = "") -> None:
@@ -35,26 +37,28 @@ class ProgressTracker:
self.wm.progress_update(progress * 100)
logger.debug(f"{self.operation_name} - {progress:.1%}: {message}")
def __enter__(self):
def __enter__(self) -> 'ProgressTracker':
logger.info(f"Starting {self.operation_name}")
return self
def __exit__(self, exc_type, exc_val, exc_tb):
def __exit__(self, exc_type: Optional[Type[BaseException]],
exc_val: Optional[BaseException],
exc_tb: Optional[Any]) -> None:
self.wm.progress_end()
logger.info(f"Completed {self.operation_name}")
def get_active_armature(context: bpy.types.Context) -> Optional[bpy.types.Object]:
def get_active_armature(context: Context) -> Optional[Object]:
"""Get the currently selected armature from Avatar Toolkit properties"""
armature_name = context.scene.avatar_toolkit.active_armature
armature_name = str(context.scene.avatar_toolkit.active_armature)
if armature_name and armature_name != 'NONE':
return bpy.data.objects.get(armature_name)
return None
def set_active_armature(context: bpy.types.Context, armature: bpy.types.Object) -> None:
def set_active_armature(context: Context, armature: Object) -> None:
"""Set the active armature for Avatar Toolkit operations"""
context.scene.avatar_toolkit.active_armature = armature
def get_armature_list(self=None, context: bpy.types.Context = None) -> List[Tuple[str, str, str]]:
def get_armature_list(self: Optional[Any] = None, context: Optional[Context] = None) -> List[Tuple[str, str, str]]:
"""Get list of all armature objects in the scene"""
if context is None:
context = bpy.context
@@ -63,25 +67,21 @@ def get_armature_list(self=None, context: bpy.types.Context = None) -> List[Tupl
return [('NONE', t("Armature.validation.no_armature"), '')]
return armatures
def validate_armature(armature: bpy.types.Object) -> Tuple[bool, List[str]]:
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] = []
# Skip validation if mode is NONE
if validation_mode == 'NONE':
return True, []
messages = []
# Basic checks always run if not NONE
if not armature or armature.type != 'ARMATURE' or not armature.data.bones:
return False, [t("Armature.validation.basic_check_failed")]
found_bones = {bone.name.lower(): bone for bone in armature.data.bones}
found_bones: Dict[str, Bone] = {bone.name.lower(): bone for bone in armature.data.bones}
essential_bones: Set[str] = {'hips', 'spine', 'chest', 'neck', 'head'}
# Essential bones check (BASIC and STRICT)
essential_bones = {'hips', 'spine', 'chest', 'neck', 'head'}
missing_bones = []
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)
@@ -89,41 +89,38 @@ def validate_armature(armature: bpy.types.Object) -> Tuple[bool, List[str]]:
if missing_bones:
messages.append(t("Armature.validation.missing_bones", bones=", ".join(missing_bones)))
# Additional checks for STRICT mode only
if validation_mode == 'STRICT':
# Hierarchy validation
hierarchy = [('hips', 'spine'), ('spine', 'chest'), ('chest', 'neck'), ('neck', 'head')]
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))
messages.append(t("Armature.validation.invalid_hierarchy",
parent=parent, child=child))
# Symmetry validation
symmetry_pairs = [('arm', 'l', 'r'), ('leg', 'l', 'r')]
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))
# Special handling for hand/wrist symmetry
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 = len(messages) == 0
is_valid: bool = len(messages) == 0
return is_valid, messages
def validate_bone_hierarchy(bones: Dict[str, bpy.types.Bone], parent_name: str, child_name: str) -> bool:
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"""
# Find matching parent and child bones using bone_names dictionary
parent_bone = None
child_bone = None
parent_bone: Optional[Bone] = None
child_bone: Optional[Bone] = None
# Check for parent bone matches
for alt_name in bone_names[parent_name]:
if alt_name in bones:
parent_bone = bones[alt_name]
break
# Check for child bone matches
for alt_name in bone_names[child_name]:
if alt_name in bones:
child_bone = bones[alt_name]
@@ -132,47 +129,42 @@ def validate_bone_hierarchy(bones: Dict[str, bpy.types.Bone], parent_name: str,
if not parent_bone or not child_bone:
return False
# Check if child's parent matches parent bone
return child_bone.parent == parent_bone
def validate_symmetry(bones: Dict[str, bpy.types.Bone], base: str, left: str, right: str) -> bool:
"""
Validate if matching left and right bones exist for a given base bone name
"""
# Define common naming patterns
left_patterns = [
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 = [
right_patterns: List[str] = [
f"{base}.{right}",
f"{base}_{right}",
f"{right}_{base}"
]
# Check if any of the patterns exist in the bones dictionary
left_exists = any(pattern in bones for pattern in left_patterns)
right_exists = any(pattern in bones for pattern in right_patterns)
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: bpy.types.Context) -> None:
def auto_select_single_armature(context: Context) -> None:
"""Automatically select armature if only one exists in scene"""
armatures = get_armature_list(context)
armatures: List[Tuple[str, str, str]] = get_armature_list(context)
if len(armatures) == 1 and armatures[0][0] != 'NONE':
toolkit = context.scene.avatar_toolkit
set_active_armature(context, armatures[0])
def clear_default_objects() -> None:
"""Removes default Blender objects (cube, light, camera)"""
"""Removes default Blender objects"""
default_names: Set[str] = {'Cube', 'Light', 'Camera'}
for obj in bpy.data.objects:
if obj.name.split('.')[0] in default_names:
bpy.data.objects.remove(obj, do_unlink=True)
def get_armature_stats(armature: bpy.types.Object) -> dict:
def get_armature_stats(armature: Object) -> Dict[str, Union[int, bool, str]]:
"""Get statistics about the armature"""
return {
'bone_count': len(armature.data.bones),
@@ -183,7 +175,7 @@ def get_armature_stats(armature: bpy.types.Object) -> dict:
def get_all_meshes(context: Context) -> List[Object]:
"""Get all mesh objects parented to the active armature"""
armature = get_active_armature(context)
armature: Optional[Object] = get_active_armature(context)
if armature:
return [obj for obj in bpy.data.objects if obj.type == 'MESH' and obj.parent == armature]
return []
@@ -196,26 +188,26 @@ def validate_mesh_for_pose(mesh_obj: Object) -> Tuple[bool, str]:
if not mesh_obj.vertex_groups:
return False, t("Mesh.validation.no_vertex_groups")
armature_mods = [mod for mod in mesh_obj.modifiers if mod.type == 'ARMATURE']
armature_mods: List[Modifier] = [mod for mod in mesh_obj.modifiers if mod.type == 'ARMATURE']
if not armature_mods:
return False, t("Mesh.validation.no_armature_modifier")
return True, t("Mesh.validation.valid")
def cache_vertex_positions(mesh_obj: Object) -> np.ndarray:
def cache_vertex_positions(mesh_obj: Object) -> npt.NDArray[np.float32]:
"""Cache vertex positions for a mesh object"""
vertices = mesh_obj.data.vertices
positions = np.empty(len(vertices) * 3, dtype=np.float32)
positions: npt.NDArray[np.float32] = np.empty(len(vertices) * 3, dtype=np.float32)
vertices.foreach_get('co', positions)
return positions.reshape(-1, 3)
def apply_vertex_positions(vertices: Object, positions: np.ndarray) -> None:
def apply_vertex_positions(vertices: Object, positions: npt.NDArray[np.float32]) -> None:
"""Apply cached vertex positions to mesh in batch"""
vertices.foreach_set('co', positions.flatten())
def process_armature_modifiers(mesh_obj: Object) -> List[Dict[str, Any]]:
"""Process and store armature modifier states"""
modifier_states = []
modifier_states: List[Dict[str, Any]] = []
for mod in mesh_obj.modifiers:
if mod.type == 'ARMATURE':
modifier_states.append({
@@ -252,10 +244,10 @@ def apply_pose_as_rest(context: Context, armature_obj: Object, meshes: List[Obje
except Exception as e:
logger.error(f"Error applying pose as rest: {str(e)}")
return False, str(e)
def apply_armature_to_mesh(armature_obj: Object, mesh_obj: Object) -> None:
"""Apply armature deformation to mesh"""
armature_mod = mesh_obj.modifiers.new('PoseToRest', 'ARMATURE')
armature_mod: Modifier = mesh_obj.modifiers.new('PoseToRest', 'ARMATURE')
armature_mod.object = armature_obj
if bpy.app.version >= (3, 5):
@@ -269,13 +261,13 @@ def apply_armature_to_mesh(armature_obj: Object, mesh_obj: Object) -> None:
def apply_armature_to_mesh_with_shapekeys(armature_obj: Object, mesh_obj: Object, context: Context) -> None:
"""Apply armature deformation to mesh with shape keys"""
old_active_index = mesh_obj.active_shape_key_index
old_show_only = mesh_obj.show_only_shape_key
old_active_index: int = mesh_obj.active_shape_key_index
old_show_only: bool = mesh_obj.show_only_shape_key
mesh_obj.show_only_shape_key = True
shape_keys = mesh_obj.data.shape_keys.key_blocks
vertex_groups = []
mutes = []
shape_keys: List[ShapeKey] = mesh_obj.data.shape_keys.key_blocks
vertex_groups: List[str] = []
mutes: List[bool] = []
for sk in shape_keys:
vertex_groups.append(sk.vertex_group)
@@ -283,17 +275,17 @@ def apply_armature_to_mesh_with_shapekeys(armature_obj: Object, mesh_obj: Object
mutes.append(sk.mute)
sk.mute = False
disabled_mods = []
disabled_mods: List[Modifier] = []
for mod in mesh_obj.modifiers:
if mod.show_viewport:
mod.show_viewport = False
disabled_mods.append(mod)
arm_mod = mesh_obj.modifiers.new('PoseToRest', 'ARMATURE')
arm_mod: Modifier = mesh_obj.modifiers.new('PoseToRest', 'ARMATURE')
arm_mod.object = armature_obj
co_length = len(mesh_obj.data.vertices) * 3
eval_cos = np.empty(co_length, dtype=np.single)
co_length: int = len(mesh_obj.data.vertices) * 3
eval_cos: npt.NDArray[np.float32] = np.empty(co_length, dtype=np.single)
for i, shape_key in enumerate(shape_keys):
mesh_obj.active_shape_key_index = i
@@ -352,12 +344,9 @@ def join_mesh_objects(context: Context, meshes: List[Object], progress: Optional
progress.step(t("Optimization.fixing_uvs"))
fix_uv_coordinates(context)
# Return the joined mesh object
return context.active_object
else:
# No objects were selected, return None
return None
return None
except Exception as e:
logger.error(f"Failed to join meshes: {str(e)}")
@@ -392,13 +381,13 @@ def fix_uv_coordinates(context: Context) -> None:
for sel_obj in current_selected:
sel_obj.select_set(True)
context.view_layer.objects.active = current_active
# This should be at the top level, not indented inside any class or function
def clear_unused_data_blocks() -> int:
"""Removes all unused data blocks from the current Blender file"""
initial_count = sum(len(getattr(bpy.data, attr)) for attr in dir(bpy.data)
initial_count: int = sum(len(getattr(bpy.data, attr)) for attr in dir(bpy.data)
if isinstance(getattr(bpy.data, attr), bpy.types.bpy_prop_collection))
bpy.ops.outliner.orphans_purge(do_local_ids=True, do_linked_ids=True, do_recursive=True)
final_count = sum(len(getattr(bpy.data, attr)) for attr in dir(bpy.data)
final_count: int = sum(len(getattr(bpy.data, attr)) for attr in dir(bpy.data)
if isinstance(getattr(bpy.data, attr), bpy.types.bpy_prop_collection))
return initial_count - final_count
@@ -408,8 +397,8 @@ def simplify_bonename(name: str) -> str:
def duplicate_bone_chain(bones: List[EditBone]) -> List[EditBone]:
"""Duplicate a chain of bones while preserving hierarchy"""
new_bones = []
parent_map = {}
new_bones: List[EditBone] = []
parent_map: Dict[EditBone, EditBone] = {}
for bone in bones:
new_bone = duplicate_bone(bone)
@@ -429,37 +418,31 @@ def restore_bone_transforms(bone: EditBone, transforms: Dict[str, Any]) -> None:
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
weights: Dict[int, float] = {}
group_index: int = 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:
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)
source_group: VertexGroup = mesh_obj.vertex_groups[source_name]
target_group: Optional[VertexGroup] = 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)
weights: Dict[int, float] = 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:
@@ -467,35 +450,30 @@ def remove_unused_shapekeys(mesh_obj: Object, tolerance: float = 0.001) -> int:
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
key_blocks: List[ShapeKey] = mesh_obj.data.shape_keys.key_blocks
vertex_count: int = len(mesh_obj.data.vertices)
removed_count: int = 0
# Cache for relative key locations
cache = {}
locations = np.empty(3 * vertex_count, dtype=np.float32)
to_delete = []
cache: Dict[str, npt.NDArray[np.float32]] = {}
locations: npt.NDArray[np.float32] = np.empty(3 * vertex_count, dtype=np.float32)
to_delete: List[str] = []
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)
rel_locations: npt.NDArray[np.float32] = 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
if not any(c in key.name for c in "-=~"):
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
@@ -503,20 +481,52 @@ def remove_unused_shapekeys(mesh_obj: Object, tolerance: float = 0.001) -> int:
return removed_count
def has_shapekeys(mesh_obj: Object) -> bool:
"""Check if mesh object has shape keys"""
return mesh_obj.data.shape_keys is not None
# Identifier to indicate that an EnumProperty is empty
# This is the default identifier used when a wrapped items function returns an empty list
# This identifier needs to be something that should never normally be used, so as to avoid the possibility of
# conflicting with an enum value that exists.
_empty_enum_identifier = 'Cats_empty_enum_identifier'
def fix_zero_length_bones(armature: Object) -> None:
"""Fix zero length bones by setting a minimum length"""
if not armature:
return
bpy.ops.object.mode_set(mode='EDIT')
for bone in armature.data.edit_bones:
if bone.length < 0.001:
bone.length = 0.001
bpy.ops.object.mode_set(mode='OBJECT')
# names - The first object will be the first one in the list. So the first one has to be the one that exists in the most models
# no_basis - If this is true the Basis will not be available in the list
def get_shapekeys(context, names, is_mouth, no_basis, return_list):
choices = []
choices_simple = []
meshes_list = get_meshes_objects(check=False)
def calculate_bone_orientation(mesh: Object, vertices: List[Any]) -> Tuple[Vector, float]:
"""Calculate optimal bone orientation based on mesh geometry"""
if not vertices:
return Vector((0, 0, 0.1)), 0.0
coords: List[Vector] = [mesh.data.vertices[v.index].co for v in vertices]
min_co: Vector = Vector(map(min, zip(*coords)))
max_co: Vector = Vector(map(max, zip(*coords)))
dimensions: Vector = max_co - min_co
roll_angle: float = 0.0
return dimensions, roll_angle
def add_armature_modifier(mesh: Object, armature: Object) -> None:
"""Add armature modifier to mesh"""
for mod in mesh.modifiers:
if mod.type == 'ARMATURE':
mesh.modifiers.remove(mod)
modifier: Modifier = mesh.modifiers.new('Armature', 'ARMATURE')
modifier.object = armature
def get_shapekeys(context: Context,
names: List[str],
is_mouth: bool,
no_basis: bool,
return_list: bool) -> Union[List[Tuple[str, str, str]], List[str]]:
"""Get shape keys based on specified criteria"""
choices: List[Tuple[str, str, str]] = []
choices_simple: List[str] = []
meshes_list: List[Object] = get_meshes_objects(check=False)
if meshes_list:
if is_mouth:
@@ -536,15 +546,12 @@ def get_shapekeys(context, names, is_mouth, no_basis, return_list):
continue
if no_basis and name == 'Basis':
continue
# 1. Will be returned by context.scene
# 2. Will be shown in lists
# 3. will be shown in the hover description (below description)
choices.append((name, name, name))
choices_simple.append(name)
_sort_enum_choices_by_identifier_lower(choices)
choices2 = []
choices2: List[Tuple[str, str, str]] = []
for name in names:
if name in choices_simple and len(choices) > 1 and choices[0][0] != name:
continue
@@ -553,22 +560,16 @@ def get_shapekeys(context, names, is_mouth, no_basis, return_list):
choices2.extend(choices)
if return_list:
shape_list = []
shape_list: List[str] = []
for choice in choices2:
shape_list.append(choice[0])
return shape_list
return choices2
# Default sorting for dynamic EnumProperty items
def _sort_enum_choices_by_identifier_lower(choices, in_place=True):
"""Sort a list of enum choices (items) by the lowercase of their identifier.
Sorting is performed in-place by default, but can be changed by setting in_place=False.
Returns the sorted list of enum choices."""
def identifier_lower(choice):
def _sort_enum_choices_by_identifier_lower(choices: List[Tuple[str, str, str]], in_place: bool = True) -> List[Tuple[str, str, str]]:
"""Sort a list of enum choices by the lowercase of their identifier"""
def identifier_lower(choice: Tuple[str, str, str]) -> str:
return choice[0].lower()
if in_place:
@@ -577,55 +578,39 @@ def _sort_enum_choices_by_identifier_lower(choices, in_place=True):
choices = sorted(choices, key=identifier_lower)
return choices
def is_enum_empty(string):
"""Returns True only if the tested string is the string that signifies that an EnumProperty is empty.
Returns False in all other cases."""
def is_enum_empty(string: str) -> bool:
"""Returns True only if the tested string is the empty enum identifier"""
return _empty_enum_identifier == string
# This function isn't needed since you can 'not is_enum_empty(string)', but is included for code clarity and readability
def is_enum_non_empty(string):
"""Returns False only if the tested string is not the string that signifies that an EnumProperty is empty.
Returns True in all other cases."""
def is_enum_non_empty(string: str) -> bool:
"""Returns False only if the tested string is not the empty enum identifier"""
return _empty_enum_identifier != string
def fix_zero_length_bones(armature: Object) -> None:
"""Fix zero length bones by setting a minimum length"""
if not armature:
return
bpy.ops.object.mode_set(mode='EDIT')
for bone in armature.data.edit_bones:
if bone.length < 0.001:
bone.length = 0.001
bpy.ops.object.mode_set(mode='OBJECT')
_empty_enum_identifier: str = 'Cats_empty_enum_identifier'
def get_meshes_objects(check: bool = True) -> List[Object]:
"""Get all mesh objects in the scene"""
meshes: List[Object] = [obj for obj in bpy.data.objects if obj.type == 'MESH']
if check and not meshes:
return []
return meshes
def calculate_bone_orientation(mesh, vertices):
"""Calculate optimal bone orientation based on mesh geometry."""
if not vertices:
return Vector((0, 0, 0.1)), 0.0
coords = [mesh.data.vertices[v.index].co for v in vertices]
min_co = Vector(map(min, zip(*coords)))
max_co = Vector(map(max, zip(*coords)))
dimensions = max_co - min_co
roll_angle = 0.0
return dimensions, roll_angle
def get_objects() -> bpy.types.BlendData:
"""Get all objects in the current Blender scene"""
return bpy.data.objects
def add_armature_modifier(mesh: Object, armature: Object):
"""Add armature modifier to mesh."""
for mod in mesh.modifiers:
if mod.type == 'ARMATURE':
mesh.modifiers.remove(mod)
modifier = mesh.modifiers.new('Armature', 'ARMATURE')
modifier.object = armature
def duplicate_bone(bone: EditBone) -> EditBone:
"""Create a duplicate of the given bone"""
new_bone: EditBone = bone.id_data.edit_bones.new(bone.name + "_copy")
new_bone.head = bone.head.copy()
new_bone.tail = bone.tail.copy()
new_bone.roll = bone.roll
new_bone.use_connect = bone.use_connect
new_bone.use_local_location = bone.use_local_location
new_bone.use_inherit_rotation = bone.use_inherit_rotation
new_bone.use_inherit_scale = bone.use_inherit_scale
new_bone.use_deform = bone.use_deform
return new_bone
#Binary tools
+3 -2
View File
@@ -1,5 +1,6 @@
import logging
from typing import Optional
from typing import Optional, Any
from bpy.types import Context
logger = logging.getLogger('avatar_toolkit')
@@ -18,7 +19,7 @@ def configure_logging(enabled: bool = False) -> None:
handler.setFormatter(formatter)
logger.addHandler(handler)
def update_logging_state(self, context) -> None:
def update_logging_state(self: Any, context: Context) -> None:
"""Update logging state based on user preference"""
from .addon_preferences import save_preference
enabled = self.enable_logging
+13 -22
View File
@@ -1,5 +1,5 @@
import bpy
from typing import List, Tuple, Optional
from typing import List, Tuple, Optional, Any, Dict, Union, Callable
from bpy.types import PropertyGroup, Material, Scene, Object, Context
from bpy.props import (
StringProperty,
@@ -18,19 +18,21 @@ from .common import get_armature_list, get_active_armature, get_all_meshes
from ..functions.visemes import VisemePreview
from ..functions.eye_tracking import set_rotation
def update_validation_mode(self, context):
def update_validation_mode(self: PropertyGroup, context: Context) -> None:
"""Updates validation mode and saves preference"""
logger.info(f"Updating validation mode to: {self.validation_mode}")
save_preference("validation_mode", self.validation_mode)
def update_logging_state(self, context):
def update_logging_state(self: PropertyGroup, context: Context) -> None:
"""Updates logging state and configures logging"""
logger.info(f"Updating logging state to: {self.enable_logging}")
save_preference("enable_logging", self.enable_logging)
from .logging_setup import configure_logging
configure_logging(self.enable_logging)
def update_shape_intensity(self, context):
def update_shape_intensity(self: PropertyGroup, context: Context) -> None:
"""Updates shape key intensity and refreshes preview"""
if self.viseme_preview_mode:
from ..functions.visemes import VisemePreview
VisemePreview.update_preview(context)
class AvatarToolkitSceneProperties(PropertyGroup):
@@ -133,13 +135,7 @@ class AvatarToolkitSceneProperties(PropertyGroup):
description=t("Visemes.preview_mode_desc"),
default=False
)
viseme_preview_selection: StringProperty(
name=t("Visemes.preview_selection"),
description=t("Visemes.preview_selection_desc"),
default="vrc.v_aa"
)
mouth_a: StringProperty(
name=t("Visemes.mouth_a"),
description=t("Visemes.mouth_a_desc")
@@ -155,6 +151,11 @@ class AvatarToolkitSceneProperties(PropertyGroup):
description=t("Visemes.mouth_ch_desc")
)
viseme_mesh: StringProperty(
name=t("Visemes.mesh_select"),
description=t("Visemes.mesh_select_desc"),
)
shape_intensity: FloatProperty(
name=t("Visemes.shape_intensity"),
description=t("Visemes.shape_intensity_desc"),
@@ -366,16 +367,6 @@ class AvatarToolkitSceneProperties(PropertyGroup):
default=True
)
attach_mesh: StringProperty(
name=t("Tools.attach_mesh_select"),
description=t("Tools.attach_mesh_select_desc")
)
attach_bone: StringProperty(
name=t("Tools.attach_bone_select"),
description=t("Tools.attach_bone_select_desc")
)
def register() -> None:
"""Register the Avatar Toolkit property group"""
logger.info("Registering Avatar Toolkit properties")