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
+89 -91
View File
@@ -1,7 +1,7 @@
import bpy
import numpy as np
from typing import List, Optional, Dict, Set
from bpy.types import Context, Object, Operator
from typing import List, Optional, Dict, Set, Tuple, Any
from bpy.types import Context, Object, Operator, ArmatureModifier, EditBone, VertexGroup, Mesh, ShapeKey
from ...core.logging_setup import logger
from ...core.translations import t
@@ -13,26 +13,27 @@ from ...core.common import (
remove_unused_shapekeys
)
class AvatarToolkit_OT_MergeArmature(Operator):
bl_idname = 'avatar_toolkit.merge_armatures'
bl_label = t('MergeArmature.label')
bl_description = t('MergeArmature.desc')
bl_options = {'REGISTER', 'UNDO'}
class AvatarToolkit_OT_MergeArmature(bpy.types.Operator):
"""Operator for merging two armatures together with their associated meshes"""
bl_idname: str = 'avatar_toolkit.merge_armatures'
bl_label: str = t('MergeArmature.label')
bl_description: str = t('MergeArmature.desc')
bl_options: Set[str] = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
def poll(cls, context: Context) -> bool:
return len(get_all_meshes(context)) > 1
def execute(self, context):
def execute(self, context: Context) -> Set[str]:
try:
wm = context.window_manager
wm.progress_begin(0, 100)
# Get both armatures
base_armature_name = context.scene.merge_armature_into
merge_armature_name = context.scene.merge_armature
base_armature = bpy.data.objects.get(base_armature_name)
merge_armature = bpy.data.objects.get(merge_armature_name)
base_armature_name: str = context.scene.merge_armature_into
merge_armature_name: str = context.scene.merge_armature
base_armature: Optional[Object] = bpy.data.objects.get(base_armature_name)
merge_armature: Optional[Object] = bpy.data.objects.get(merge_armature_name)
if not base_armature or not merge_armature:
logger.error(f"Armature not found: {merge_armature_name}")
@@ -51,15 +52,15 @@ class AvatarToolkit_OT_MergeArmature(Operator):
wm.progress_update(80)
# Get settings from scene properties
merge_all_bones = context.scene.avatar_toolkit.merge_all_bones
join_meshes = context.scene.avatar_toolkit.join_meshes
merge_all_bones: bool = context.scene.avatar_toolkit.merge_all_bones
join_meshes: bool = context.scene.avatar_toolkit.join_meshes
# Merge armatures
merge_armatures(
base_armature_name,
merge_armature_name,
mesh_only=False,
merge_all_bones=context.scene.avatar_toolkit.merge_all_bones,
merge_all_bones=merge_all_bones,
join_meshes=join_meshes,
operator=self
)
@@ -76,10 +77,10 @@ class AvatarToolkit_OT_MergeArmature(Operator):
self.report({'ERROR'}, str(e))
return {'CANCELLED'}
def delete_rigidbodies_and_joints(armature: Object):
"""Delete rigid bodies and joints associated with the armature."""
to_delete = []
parent = armature
def delete_rigidbodies_and_joints(armature: Object) -> None:
"""Delete rigid bodies and joints associated with an armature"""
to_delete: List[Object] = []
parent: Object = armature
while parent.parent:
parent = parent.parent
@@ -94,9 +95,9 @@ def delete_rigidbodies_and_joints(armature: Object):
bpy.data.objects.remove(obj, do_unlink=True)
def validate_parents_and_transforms(merge_armature: Object, base_armature: Object, context: Context) -> bool:
"""Validate parents and transformations of armatures before merging."""
merge_parent = merge_armature.parent
base_parent = base_armature.parent
"""Validate parent relationships and transformations of armatures"""
merge_parent: Optional[Object] = merge_armature.parent
base_parent: Optional[Object] = base_armature.parent
if merge_parent or base_parent:
if context.scene.merge_all_bones:
@@ -112,21 +113,21 @@ def validate_parents_and_transforms(merge_armature: Object, base_armature: Objec
return True
def is_transform_clean(obj: Object) -> bool:
"""Check if an object's transforms are at default values."""
"""Check if object transforms are at default values"""
for i in range(3):
if obj.scale[i] != 1 or obj.location[i] != 0 or obj.rotation_euler[i] != 0:
return False
return True
def prepare_mesh_vertex_groups(mesh: Object):
"""Prepare mesh by assigning all vertices to a new vertex group."""
def prepare_mesh_vertex_groups(mesh: Object) -> None:
"""Initialize mesh vertex groups for merging process"""
if mesh.vertex_groups:
for vg in mesh.vertex_groups:
mesh.vertex_groups.remove(vg)
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action='SELECT')
vg = mesh.vertex_groups.new(name=mesh.name)
vg: VertexGroup = mesh.vertex_groups.new(name=mesh.name)
bpy.ops.object.vertex_group_assign()
bpy.ops.object.mode_set(mode='OBJECT')
@@ -136,14 +137,14 @@ def merge_armatures(
mesh_only: bool,
merge_all_bones: bool = False,
join_meshes: bool = False,
operator=None
):
"""Main function to merge two armatures."""
operator: Optional[Operator] = None
) -> None:
"""Main function to merge two armatures with their associated meshes and data"""
logger.info(f"Merging armatures: {merge_armature_name} into {base_armature_name}")
tolerance = 0.00008726647 # around 0.005 degrees
tolerance: float = 0.00008726647 # around 0.005 degrees
base_armature = bpy.data.objects.get(base_armature_name)
merge_armature = bpy.data.objects.get(merge_armature_name)
base_armature: Optional[Object] = bpy.data.objects.get(base_armature_name)
merge_armature: Optional[Object] = bpy.data.objects.get(merge_armature_name)
if not base_armature or not merge_armature:
logger.error(f"Armature not found: {merge_armature_name}")
@@ -172,12 +173,12 @@ def merge_armatures(
fix_zero_length_bones(merge_armature)
# Store original parent relationships
original_parents = {}
original_parents: Dict[str, Optional[str]] = {}
for bone in merge_armature.data.bones:
original_parents[bone.name] = bone.parent.name if bone.parent else None
# Get base bone names
base_bone_names = set(bone.name for bone in base_armature.data.bones)
base_bone_names: Set[str] = {bone.name for bone in base_armature.data.bones}
# Switch to edit mode on merge armature and rename bones
bpy.context.view_layer.objects.active = merge_armature
@@ -206,11 +207,11 @@ def merge_armatures(
# Restore parent relationships
bpy.ops.object.mode_set(mode='EDIT')
for bone in base_armature.data.edit_bones:
base_name = bone.name.replace('.merge', '')
base_name: str = bone.name.replace('.merge', '')
if base_name in original_parents:
parent_name = original_parents[base_name]
parent_name: Optional[str] = original_parents[base_name]
if parent_name:
parent_bone = base_armature.data.edit_bones.get(parent_name)
parent_bone: Optional[EditBone] = base_armature.data.edit_bones.get(parent_name)
if parent_bone:
bone.parent = parent_bone
@@ -223,7 +224,7 @@ def merge_armatures(
# Process vertex groups if not mesh_only
if not mesh_only:
meshes = [obj for obj in bpy.data.objects if obj.type == 'MESH' and obj.parent == base_armature]
meshes: List[Object] = [obj for obj in bpy.data.objects if obj.type == 'MESH' and obj.parent == base_armature]
process_vertex_groups(meshes)
# Remove zero weight vertex groups if enabled
@@ -235,9 +236,9 @@ def merge_armatures(
# Join meshes if requested
if join_meshes:
meshes_to_join = [obj for obj in bpy.data.objects if obj.type == 'MESH' and obj.parent == base_armature]
meshes_to_join: List[Object] = [obj for obj in bpy.data.objects if obj.type == 'MESH' and obj.parent == base_armature]
if meshes_to_join:
joined_mesh = join_mesh_objects(bpy.context, meshes_to_join)
joined_mesh: Optional[Object] = join_mesh_objects(bpy.context, meshes_to_join)
if joined_mesh:
logger.info(f"Joined meshes into {joined_mesh.name}")
@@ -250,8 +251,8 @@ def merge_armatures(
# Remove any remaining .merge bones
bpy.context.view_layer.objects.active = base_armature
bpy.ops.object.mode_set(mode='EDIT')
edit_bones = base_armature.data.edit_bones
bones_to_remove = [bone for bone in edit_bones if bone.name.endswith('.merge')]
edit_bones: List[EditBone] = base_armature.data.edit_bones
bones_to_remove: List[EditBone] = [bone for bone in edit_bones if bone.name.endswith('.merge')]
for bone in bones_to_remove:
edit_bones.remove(bone)
bpy.ops.object.mode_set(mode='OBJECT')
@@ -259,14 +260,13 @@ def merge_armatures(
# Final cleanup
clear_unused_data_blocks()
def validate_merge_armature_transforms(
base_armature: Object,
merge_armature: Object,
mesh_merge: Optional[Object],
tolerance: float
) -> bool:
"""Validate transforms of both armatures and mesh."""
"""Validate transforms of both armatures and mesh"""
for i in [0, 1, 2]:
if abs(base_armature.scale[i] - merge_armature.scale[i]) > tolerance:
return False
@@ -280,10 +280,10 @@ def validate_merge_armature_transforms(
def adjust_merge_armature_transforms(
merge_armature: Object,
mesh_merge: Object
):
"""Adjust transforms of the merge armature."""
old_loc = list(merge_armature.location)
old_scale = list(merge_armature.scale)
) -> None:
"""Adjust transforms of the merge armature"""
old_loc: List[float] = list(merge_armature.location)
old_scale: List[float] = list(merge_armature.scale)
for i in [0, 1, 2]:
merge_armature.location[i] = (mesh_merge.location[i] * old_scale[i]) + old_loc[i]
@@ -295,25 +295,24 @@ def adjust_merge_armature_transforms(
mesh_merge.rotation_euler[i] = 0
mesh_merge.scale[i] = 1
def detect_bones_to_merge(
base_edit_bones: bpy.types.ArmatureEditBones,
merge_edit_bones: bpy.types.ArmatureEditBones,
tolerance: float,
merge_all_bones: bool
) -> List[str]:
"""Detect corresponding bones between base and merge armatures using smart detection and position tolerance."""
bones_to_merge = []
"""Detect corresponding bones between base and merge armatures using smart detection and position tolerance"""
bones_to_merge: List[str] = []
# Cache base bone positions
base_bones_positions = {
base_bones_positions: Dict[str, np.ndarray] = {
bone.name: np.array(bone.head) for bone in base_edit_bones
}
# Smart bone detection
for merge_bone in merge_edit_bones:
merge_bone_position = np.array(merge_bone.head)
found_match = False
merge_bone_position: np.ndarray = np.array(merge_bone.head)
found_match: bool = False
if merge_all_bones and merge_bone.name in base_bones_positions:
# If merging same bones by name
@@ -333,17 +332,16 @@ def detect_bones_to_merge(
return bones_to_merge
def process_vertex_groups(meshes: List[Object]):
"""Process vertex groups in meshes."""
def process_vertex_groups(meshes: List[Object]) -> None:
"""Process vertex groups in meshes"""
for mesh in meshes:
vg_names = {vg.name for vg in mesh.vertex_groups}
merge_vg_names = [vg_name for vg_name in vg_names if vg_name.endswith('.merge')]
vg_names: Set[str] = {vg.name for vg in mesh.vertex_groups}
merge_vg_names: List[str] = [vg_name for vg_name in vg_names if vg_name.endswith('.merge')]
for vg_merge_name in merge_vg_names:
base_name = vg_merge_name[:-6]
vg_merge = mesh.vertex_groups.get(vg_merge_name)
vg_base = mesh.vertex_groups.get(base_name)
base_name: str = vg_merge_name[:-6]
vg_merge: Optional[VertexGroup] = mesh.vertex_groups.get(vg_merge_name)
vg_base: Optional[VertexGroup] = mesh.vertex_groups.get(base_name)
if vg_merge is None:
continue
@@ -353,20 +351,20 @@ def process_vertex_groups(meshes: List[Object]):
else:
vg_merge.name = base_name
def mix_vertex_groups(mesh: Object, vg_from_name: str, vg_to_name: str):
"""Mix vertex group weights."""
vg_from = mesh.vertex_groups.get(vg_from_name)
vg_to = mesh.vertex_groups.get(vg_to_name)
def mix_vertex_groups(mesh: Object, vg_from_name: str, vg_to_name: str) -> None:
"""Mix vertex group weights"""
vg_from: Optional[VertexGroup] = mesh.vertex_groups.get(vg_from_name)
vg_to: Optional[VertexGroup] = mesh.vertex_groups.get(vg_to_name)
if not vg_from or not vg_to:
return
num_vertices = len(mesh.data.vertices)
weights_from = np.zeros(num_vertices)
weights_to = np.zeros(num_vertices)
num_vertices: int = len(mesh.data.vertices)
weights_from: np.ndarray = np.zeros(num_vertices)
weights_to: np.ndarray = np.zeros(num_vertices)
idx_from = vg_from.index
idx_to = vg_to.index
idx_from: int = vg_from.index
idx_to: int = vg_to.index
for v in mesh.data.vertices:
for g in v.groups:
@@ -375,14 +373,14 @@ def mix_vertex_groups(mesh: Object, vg_from_name: str, vg_to_name: str):
elif g.group == idx_to:
weights_to[v.index] = g.weight
weights_combined = np.clip(weights_from + weights_to, 0.0, 1.0)
weights_combined: np.ndarray = np.clip(weights_from + weights_to, 0.0, 1.0)
vg_to.add(range(num_vertices), weights_combined.tolist(), 'REPLACE')
mesh.vertex_groups.remove(vg_from)
def remove_unused_vertex_groups(mesh: Object):
"""Remove vertex groups with no weights."""
def remove_unused_vertex_groups(mesh: Object) -> None:
"""Remove vertex groups with no weights"""
for vg in mesh.vertex_groups:
has_weights = False
has_weights: bool = False
for vert in mesh.data.vertices:
for group in vert.groups:
if group.group == vg.index and group.weight > 0.001:
@@ -393,9 +391,9 @@ def remove_unused_vertex_groups(mesh: Object):
if not has_weights:
mesh.vertex_groups.remove(vg)
def apply_armature_to_mesh(armature: Object, mesh: Object):
"""Apply armature deformation to mesh."""
armature_mod = mesh.modifiers.new('PoseToRest', 'ARMATURE')
def apply_armature_to_mesh(armature: Object, mesh: Object) -> None:
"""Apply armature deformation to mesh"""
armature_mod: ArmatureModifier = mesh.modifiers.new('PoseToRest', 'ARMATURE')
armature_mod.object = armature
if bpy.app.version >= (3, 5):
@@ -407,15 +405,15 @@ def apply_armature_to_mesh(armature: Object, mesh: Object):
with bpy.context.temp_override(object=mesh):
bpy.ops.object.modifier_apply(modifier=armature_mod.name)
def apply_armature_to_mesh_with_shapekeys(armature: Object, mesh: Object, context: Context):
"""Apply armature deformation to mesh with shape keys."""
old_active_index = mesh.active_shape_key_index
old_show_only = mesh.show_only_shape_key
def apply_armature_to_mesh_with_shapekeys(armature: Object, mesh: Object, context: Context) -> None:
"""Apply armature deformation to mesh with shape keys"""
old_active_index: int = mesh.active_shape_key_index
old_show_only: bool = mesh.show_only_shape_key
mesh.show_only_shape_key = True
shape_keys = mesh.data.shape_keys.key_blocks
vertex_groups = []
mutes = []
shape_keys: List[ShapeKey] = mesh.data.shape_keys.key_blocks
vertex_groups: List[str] = []
mutes: List[bool] = []
for sk in shape_keys:
vertex_groups.append(sk.vertex_group)
@@ -423,23 +421,23 @@ def apply_armature_to_mesh_with_shapekeys(armature: Object, mesh: Object, contex
mutes.append(sk.mute)
sk.mute = False
disabled_mods = []
disabled_mods: List[Any] = []
for mod in mesh.modifiers:
if mod.show_viewport:
mod.show_viewport = False
disabled_mods.append(mod)
arm_mod = mesh.modifiers.new('PoseToRest', 'ARMATURE')
arm_mod: ArmatureModifier = mesh.modifiers.new('PoseToRest', 'ARMATURE')
arm_mod.object = armature
co_length = len(mesh.data.vertices) * 3
eval_cos = np.empty(co_length, dtype=np.single)
co_length: int = len(mesh.data.vertices) * 3
eval_cos: np.ndarray = np.empty(co_length, dtype=np.single)
for i, shape_key in enumerate(shape_keys):
mesh.active_shape_key_index = i
depsgraph = context.evaluated_depsgraph_get()
eval_mesh = mesh.evaluated_get(depsgraph)
eval_mesh: Mesh = mesh.evaluated_get(depsgraph)
eval_mesh.data.vertices.foreach_get('co', eval_cos)
shape_key.data.foreach_set('co', eval_cos)
+30 -22
View File
@@ -1,7 +1,7 @@
import bpy
from bpy.types import Operator, Context, Object
from bpy.types import Operator, Context, Object, ArmatureModifier, VertexGroup
from mathutils import Vector
from typing import Set, Optional
from typing import Set, Optional, List, Any
from ...core.logging_setup import logger
from ...core.translations import t
@@ -15,28 +15,34 @@ from ...core.common import (
)
class AvatarToolkit_OT_AttachMesh(Operator):
"""Attach a mesh to an armature bone with automatic weight setup"""
bl_idname = "avatar_toolkit.attach_mesh"
bl_label = t("AttachMesh.label")
bl_description = t("AttachMesh.desc")
bl_options = {'REGISTER', 'UNDO'}
"""Operator to attach a mesh to an armature bone with automatic weight setup"""
bl_idname: str = "avatar_toolkit.attach_mesh"
bl_label: str = t("AttachMesh.label")
bl_description: str = t("AttachMesh.desc")
bl_options: Set[str] = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context: Context) -> bool:
armature = get_active_armature(context)
return armature is not None and context.mode == 'OBJECT' and len(get_all_meshes(context)) > 0
"""Check if operator can be executed"""
armature: Optional[Object] = 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:
logger.info("Starting mesh attachment process")
mesh_name = context.scene.avatar_toolkit.attach_mesh
armature = get_active_armature(context)
attach_bone_name = context.scene.avatar_toolkit.attach_bone
mesh = bpy.data.objects.get(mesh_name)
mesh_name: str = context.scene.avatar_toolkit.attach_mesh
armature: Object = get_active_armature(context)
attach_bone_name: str = context.scene.avatar_toolkit.attach_bone
mesh: Optional[Object] = bpy.data.objects.get(mesh_name)
with ProgressTracker(context, 10, "Attaching Mesh") as progress:
# Validation steps
is_valid: bool
error_msg: str
is_valid, error_msg = validate_mesh_transforms(mesh)
if not is_valid:
raise ValueError(error_msg)
@@ -63,7 +69,7 @@ class AvatarToolkit_OT_AttachMesh(Operator):
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action='SELECT')
vg = mesh.vertex_groups.new(name=mesh_name)
vg: VertexGroup = mesh.vertex_groups.new(name=mesh_name)
bpy.ops.object.vertex_group_assign()
bpy.ops.object.mode_set(mode='OBJECT')
progress.step(t("AttachMesh.setup_weights"))
@@ -83,12 +89,14 @@ class AvatarToolkit_OT_AttachMesh(Operator):
progress.step(t("AttachMesh.create_bone"))
# Calculate bone placement
verts_in_group = [v for v in mesh.data.vertices
verts_in_group: List[Any] = [v for v in mesh.data.vertices
for g in v.groups if g.group == vg.index]
dimensions: Vector
roll_angle: float
dimensions, roll_angle = calculate_bone_orientation(mesh, verts_in_group)
# Set bone position and orientation
center = Vector((0, 0, 0))
center: Vector = Vector((0, 0, 0))
for v in verts_in_group:
center += mesh.data.vertices[v.index].co
center /= len(verts_in_group)
@@ -111,20 +119,20 @@ class AvatarToolkit_OT_AttachMesh(Operator):
self.report({'ERROR'}, str(e))
return {'CANCELLED'}
def validate_mesh_transforms(mesh):
"""Validate mesh transforms are suitable for attaching."""
def validate_mesh_transforms(mesh: Optional[Object]) -> tuple[bool, str]:
"""Validate mesh transforms are suitable for attaching"""
if not mesh:
return False, "Mesh not found"
# Check for non-uniform scale
scale = mesh.scale
scale: Vector = mesh.scale
if abs(scale[0] - scale[1]) > 0.001 or abs(scale[1] - scale[2]) > 0.001:
return False, "Mesh has non-uniform scale. Please apply scale (Ctrl+A)"
return True, ""
def validate_mesh_name(armature, mesh_name):
"""Validate mesh name doesn't conflict with existing bones."""
def validate_mesh_name(armature: Object, mesh_name: str) -> tuple[bool, str]:
"""Validate mesh name doesn't conflict with existing bones"""
if mesh_name in armature.data.bones:
return False, f"Bone named '{mesh_name}' already exists in armature"
return True, ""
return True, ""
+96 -80
View File
@@ -5,8 +5,8 @@ import math
import bmesh
import mathutils
import json
from bpy.types import Operator, Object, Context
from typing import Optional, Dict, Tuple, Set
from bpy.types import Operator, Object, Context, UILayout, WindowManager, Event, ShapeKey, EditBone, PoseBone
from typing import Optional, Dict, Tuple, Set, List, Any, Union, ClassVar
from collections import OrderedDict
from random import random
from itertools import chain
@@ -24,19 +24,19 @@ from ..core.common import (
apply_vertex_positions
)
VALID_EYE_NAMES = {
VALID_EYE_NAMES: Dict[str, List[str]] = {
'left': ['LeftEye', 'Eye_L', 'eye_L', 'eye.L', 'EyeLeft', 'left_eye', 'l_eye'],
'right': ['RightEye', 'Eye_R', 'eye_R', 'eye.R', 'EyeRight', 'right_eye', 'r_eye']
}
class CreateEyesAV3Button(bpy.types.Operator):
"""Create eye tracking setup for VRChat Avatar 3.0"""
bl_idname = 'avatar_toolkit.create_eye_tracking_av3'
bl_label = t('EyeTracking.create.av3.label')
bl_description = t('EyeTracking.create.av3.desc')
bl_options = {'REGISTER', 'UNDO'}
"""Creates eye tracking setup compatible with VRChat Avatar 3.0 system"""
bl_idname: str = 'avatar_toolkit.create_eye_tracking_av3'
bl_label: str = t('EyeTracking.create.av3.label')
bl_description: str = t('EyeTracking.create.av3.desc')
bl_options: Set[str] = {'REGISTER', 'UNDO'}
mesh = None
mesh: Optional[Object] = None
@classmethod
def poll(cls, context):
@@ -109,13 +109,13 @@ class CreateEyesAV3Button(bpy.types.Operator):
return {'CANCELLED'}
class CreateEyesSDK2Button(bpy.types.Operator):
"""Create eye tracking setup for VRChat SDK2"""
bl_idname = 'avatar_toolkit.create_eye_tracking_sdk2'
bl_label = t('EyeTracking.create.sdk2.label')
bl_description = t('EyeTracking.create.sdk2.desc')
bl_options = {'REGISTER', 'UNDO'}
"""Creates eye tracking setup compatible with VRChat SDK2 system"""
bl_idname: str = 'avatar_toolkit.create_eye_tracking_sdk2'
bl_label: str = t('EyeTracking.create.sdk2.label')
bl_description: str = t('EyeTracking.create.sdk2.desc')
bl_options: Set[str] = {'REGISTER', 'UNDO'}
mesh = None
mesh: Optional[Object] = None
@classmethod
def poll(cls, context):
@@ -201,8 +201,9 @@ class CreateEyesSDK2Button(bpy.types.Operator):
return {'CANCELLED'}
class EyeTrackingBackup:
def __init__(self):
self.backup_path = os.path.join(bpy.app.tempdir, "eye_tracking_backup.json")
"""Manages backup and restoration of eye bone positions"""
def __init__(self) -> None:
self.backup_path: str = os.path.join(bpy.app.tempdir, "eye_tracking_backup.json")
self.bone_positions: Dict[str, Dict[str, Tuple[float, float, float]]] = {}
def store_bone_positions(self, armature) -> bool:
@@ -247,8 +248,10 @@ class EyeTrackingBackup:
return False
class EyeTrackingValidator:
"""Validates eye tracking setup requirements and configurations"""
@staticmethod
def find_eye_vertex_groups(mesh_name: str) -> Tuple[str, str]:
def find_eye_vertex_groups(mesh_name: str) -> Tuple[Optional[str], Optional[str]]:
"""Locates left and right eye vertex groups in mesh"""
mesh = bpy.data.objects.get(mesh_name)
if not mesh:
return None, None
@@ -265,7 +268,8 @@ class EyeTrackingValidator:
return left_group, right_group
@staticmethod
def validate_setup(context, mesh_name: str) -> Tuple[bool, str]:
def validate_setup(context: Context, mesh_name: str) -> Tuple[bool, str]:
"""Validates complete eye tracking setup configuration"""
armature = get_active_armature(context)
if not armature:
return False, t('EyeTracking.validation.noArmature')
@@ -299,10 +303,11 @@ class EyeTrackingValidator:
return True, t('EyeTracking.validation.success')
class StartTestingButton(bpy.types.Operator):
bl_idname = 'avatar_toolkit.start_eye_testing'
bl_label = t('EyeTracking.testing.start.label')
bl_description = t('EyeTracking.testing.start.desc')
bl_options = {'REGISTER', 'UNDO'}
"""Initiates eye tracking testing mode"""
bl_idname: str = 'avatar_toolkit.start_eye_testing'
bl_label: str = t('EyeTracking.testing.start.label')
bl_description: str = t('EyeTracking.testing.start.desc')
bl_options: Set[str] = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
@@ -351,10 +356,11 @@ class StartTestingButton(bpy.types.Operator):
return {'FINISHED'}
class StopTestingButton(bpy.types.Operator):
bl_idname = 'avatar_toolkit.stop_eye_testing'
bl_label = t('EyeTracking.testing.stop.label')
bl_description = t('EyeTracking.testing.stop.desc')
bl_options = {'REGISTER', 'UNDO'}
"""Terminates eye tracking testing mode"""
bl_idname: str = 'avatar_toolkit.stop_eye_testing'
bl_label: str = t('EyeTracking.testing.stop.label')
bl_description: str = t('EyeTracking.testing.stop.desc')
bl_options: Set[str] = {'REGISTER', 'UNDO'}
def execute(self, context):
global eye_left, eye_right, eye_left_data, eye_right_data, eye_left_rot, eye_right_rot
@@ -392,6 +398,7 @@ class StopTestingButton(bpy.types.Operator):
return {'FINISHED'}
def set_rotation(self, context):
"""Updates eye bone rotations based on current settings"""
global eye_left, eye_right, eye_left_rot, eye_right_rot
toolkit = context.scene.avatar_toolkit
@@ -414,10 +421,11 @@ def set_rotation(self, context):
return None
class ResetRotationButton(bpy.types.Operator):
bl_idname = 'avatar_toolkit.reset_eye_rotation'
bl_label = t('EyeTracking.reset.label')
bl_description = t('EyeTracking.reset.desc')
bl_options = {'REGISTER', 'UNDO'}
"""Resets eye bone rotations to default values"""
bl_idname: str = 'avatar_toolkit.reset_eye_rotation'
bl_label: str = t('EyeTracking.reset.label')
bl_description: str = t('EyeTracking.reset.desc')
bl_options: Set[str] = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
@@ -445,10 +453,11 @@ class ResetRotationButton(bpy.types.Operator):
return {'FINISHED'}
class AdjustEyesButton(bpy.types.Operator):
bl_idname = 'avatar_toolkit.adjust_eyes'
bl_label = t('EyeTracking.adjust.label')
bl_description = t('EyeTracking.adjust.desc')
bl_options = {'REGISTER', 'UNDO'}
"""Adjusts eye bone positions and orientations"""
bl_idname: str = 'avatar_toolkit.adjust_eyes'
bl_label: str = t('EyeTracking.adjust.label')
bl_description: str = t('EyeTracking.adjust.desc')
bl_options: Set[str] = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
@@ -494,10 +503,11 @@ class AdjustEyesButton(bpy.types.Operator):
return {'FINISHED'}
class StartIrisHeightButton(bpy.types.Operator):
bl_idname = 'avatar_toolkit.adjust_iris_height'
bl_label = t('EyeTracking.iris.label')
bl_description = t('EyeTracking.iris.desc')
bl_options = {'REGISTER', 'UNDO'}
"""Adjusts iris height for eye meshes"""
bl_idname: str = 'avatar_toolkit.adjust_iris_height'
bl_label: str = t('EyeTracking.iris.label')
bl_description: str = t('EyeTracking.iris.desc')
bl_options: Set[str] = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
@@ -536,10 +546,11 @@ class StartIrisHeightButton(bpy.types.Operator):
return {'FINISHED'}
class TestBlinking(bpy.types.Operator):
bl_idname = 'avatar_toolkit.test_blinking'
bl_label = t('EyeTracking.blink.test.label')
bl_description = t('EyeTracking.blink.test.desc')
bl_options = {'REGISTER', 'UNDO'}
"""Tests eye blinking animations"""
bl_idname: str = 'avatar_toolkit.test_blinking'
bl_label: str = t('EyeTracking.blink.test.label')
bl_description: str = t('EyeTracking.blink.test.desc')
bl_options: Set[str] = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
@@ -559,10 +570,11 @@ class TestBlinking(bpy.types.Operator):
return {'FINISHED'}
class TestLowerlid(bpy.types.Operator):
bl_idname = 'avatar_toolkit.test_lowerlid'
bl_label = t('EyeTracking.lowerlid.test.label')
bl_description = t('EyeTracking.lowerlid.test.desc')
bl_options = {'REGISTER', 'UNDO'}
"""Tests lower eyelid movements"""
bl_idname: str = 'avatar_toolkit.test_lowerlid'
bl_label: str = t('EyeTracking.lowerlid.test.label')
bl_description: str = t('EyeTracking.lowerlid.test.desc')
bl_options: Set[str] = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
@@ -584,10 +596,11 @@ class TestLowerlid(bpy.types.Operator):
return {'FINISHED'}
class ResetBlinkTest(bpy.types.Operator):
bl_idname = 'avatar_toolkit.reset_blink_test'
bl_label = t('EyeTracking.blink.reset.label')
bl_description = t('EyeTracking.blink.reset.desc')
bl_options = {'REGISTER', 'UNDO'}
"""Resets all eye blinking test values"""
bl_idname: str = 'avatar_toolkit.reset_blink_test'
bl_label: str = t('EyeTracking.blink.reset.label')
bl_description: str = t('EyeTracking.blink.reset.desc')
bl_options: Set[str] = {'REGISTER', 'UNDO'}
def execute(self, context):
toolkit = context.scene.avatar_toolkit
@@ -601,7 +614,8 @@ class ResetBlinkTest(bpy.types.Operator):
return {'FINISHED'}
def fix_eye_position(context, old_eye, new_eye, head, right_side):
def fix_eye_position(context: Context, old_eye: Union[EditBone, PoseBone], new_eye: EditBone, head: Optional[EditBone], right_side: bool) -> None:
"""Adjusts eye bone positions and orientations for proper tracking"""
toolkit = context.scene.avatar_toolkit
scale = -toolkit.eye_distance + 1
mesh = bpy.data.objects[toolkit.mesh_name_eye]
@@ -637,8 +651,8 @@ def fix_eye_position(context, old_eye, new_eye, head, right_side):
new_eye.tail[y_cord] = new_eye.head[y_cord]
new_eye.tail[z_cord] = new_eye.head[z_cord] + 0.1
def repair_shapekeys(mesh_name, vertex_group):
"""Fix VRC shape keys by slightly adjusting vertex positions"""
def repair_shapekeys(mesh_name: str, vertex_group: str) -> None:
"""Repairs VRChat shape keys by adjusting vertex positions"""
armature = get_active_armature(bpy.context)
mesh = bpy.data.objects[mesh_name]
mesh.select_set(True)
@@ -696,10 +710,12 @@ def repair_shapekeys(mesh_name, vertex_group):
logger.warning('Shape key repair failed, using random method')
repair_shapekeys_mouth(mesh_name)
def randBoolNumber():
def randBoolNumber() -> int:
"""Generates random boolean value as integer"""
return -1 if random() < 0.5 else 1
def repair_shapekeys_mouth(mesh_name):
def repair_shapekeys_mouth(mesh_name: str) -> None:
"""Repairs mouth-related shape keys using fallback method"""
mesh = bpy.data.objects[mesh_name]
mesh.select_set(True)
bpy.context.view_layer.objects.active = mesh
@@ -730,12 +746,12 @@ def repair_shapekeys_mouth(mesh_name):
if not moved:
logger.error('Random shape key repair failed')
def get_bone_orientations():
"""Get bone orientation axes"""
def get_bone_orientations() -> Tuple[int, int, int]:
"""Returns standardized bone orientation axes"""
return (0, 1, 2) # x, y, z coordinates
def find_center_vector_of_vertex_group(mesh, group_name):
"""Calculate center position of vertex group"""
def find_center_vector_of_vertex_group(mesh: Object, group_name: str) -> Union[mathutils.Vector, bool]:
"""Calculates center position of vertex group"""
group = mesh.vertex_groups.get(group_name)
if not group:
return False
@@ -751,8 +767,8 @@ def find_center_vector_of_vertex_group(mesh, group_name):
return sum((v for v in vertices), mathutils.Vector()) / len(vertices)
def vertex_group_exists(mesh_obj, group_name):
"""Check if vertex group exists and has weights"""
def vertex_group_exists(mesh_obj: Object, group_name: str) -> bool:
"""Verifies existence and validity of vertex group"""
if not mesh_obj or group_name not in mesh_obj.vertex_groups:
return False
@@ -763,8 +779,8 @@ def vertex_group_exists(mesh_obj, group_name):
return True
return False
def copy_vertex_group(self, vertex_group, rename_to):
"""Copy vertex group with new name"""
def copy_vertex_group(self: Any, vertex_group: str, rename_to: str) -> None:
"""Creates copy of vertex group with new name"""
vertex_group_index = 0
# Select and make mesh active
bpy.ops.object.mode_set(mode='OBJECT')
@@ -781,8 +797,8 @@ def copy_vertex_group(self, vertex_group, rename_to):
vertex_group_index += 1
def copy_shape_key(self, context, from_shape, new_names, new_index):
"""Copy shape key with new name"""
def copy_shape_key(self: Any, context: Context, from_shape: str, new_names: List[str], new_index: int) -> str:
"""Creates copy of shape key with new name"""
blinking = not context.scene.avatar_toolkit.disable_eye_blinking
new_name = new_names[new_index - 1]
@@ -847,11 +863,11 @@ class VertexGroupCache:
cls._cache.clear()
class RotateEyeBonesForAv3Button(Operator):
"""Reorient eye bones for proper VRChat eye tracking"""
bl_idname = "avatar_toolkit.rotate_eye_bones"
bl_label = t("EyeTracking.rotate.label")
bl_description = t("EyeTracking.rotate.desc")
bl_options = {'REGISTER', 'UNDO'}
"""Reorients eye bones for VRChat Avatar 3.0 compatibility"""
bl_idname: str = "avatar_toolkit.rotate_eye_bones"
bl_label: str = t("EyeTracking.rotate.label")
bl_description: str = t("EyeTracking.rotate.desc")
bl_options: Set[str] = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
@@ -874,11 +890,11 @@ class RotateEyeBonesForAv3Button(Operator):
return {'FINISHED'}
class ResetEyeTrackingButton(Operator):
"""Reset all eye tracking settings and state"""
bl_idname = 'avatar_toolkit.reset_eye_tracking'
bl_label = t('EyeTracking.reset.label')
bl_description = t('EyeTracking.reset.desc')
bl_options = {'REGISTER', 'UNDO'}
"""Resets all eye tracking settings to default values"""
bl_idname: str = 'avatar_toolkit.reset_eye_tracking'
bl_label: str = t('EyeTracking.reset.label')
bl_description: str = t('EyeTracking.reset.desc')
bl_options: Set[str] = {'REGISTER', 'UNDO'}
def execute(self, context):
global eye_left, eye_right, eye_left_data, eye_right_data, eye_left_rot, eye_right_rot
@@ -888,7 +904,7 @@ class ResetEyeTrackingButton(Operator):
return {'FINISHED'}
def validate_weights(mesh_obj: Object, vertex_group: str) -> bool:
"""Validate vertex group weights"""
"""Validates vertex group weight assignments"""
group = mesh_obj.vertex_groups.get(vertex_group)
if not group:
return False
@@ -899,8 +915,8 @@ def validate_weights(mesh_obj: Object, vertex_group: str) -> bool:
return True
return False
def get_eye_bone_names(armature: Object) -> Dict[str, str]:
"""Get standardized eye bone names"""
def get_eye_bone_names(armature: Object) -> Dict[str, Optional[str]]:
"""Retrieves standardized eye bone names from armature"""
eye_bones = {'left': None, 'right': None}
for bone in armature.data.bones:
@@ -912,7 +928,7 @@ def get_eye_bone_names(armature: Object) -> Dict[str, str]:
return eye_bones
def stop_testing(context: Context) -> None:
"""Stop eye tracking testing mode"""
"""Stops eye tracking testing mode and resets all values"""
global eye_left, eye_right, eye_left_data, eye_right_data, eye_left_rot, eye_right_rot
if not all([eye_left, eye_right, eye_left_data, eye_right_data, eye_left_rot, eye_right_rot]):
@@ -81,6 +81,9 @@ class AvatarToolkit_OT_CombineMaterials(Operator):
@classmethod
def poll(cls, context: Context) -> bool:
"""Check if the operator can be executed"""
if context.mode != 'OBJECT':
return False
armature = get_active_armature(context)
if not armature:
return False
+4
View File
@@ -134,6 +134,10 @@ class AvatarToolKit_OT_DeleteBoneConstraints(Operator):
def execute(self, context: Context) -> set[str]:
"""Execute the constraint removal operation"""
# Make sure we are in Object mode first or it will error
bpy.ops.object.mode_set(mode='OBJECT')
armature = get_active_armature(context)
# Select armature and make it active before changing mode
+23 -16
View File
@@ -1,9 +1,8 @@
# MIT License
# This code was taken from Cats Blender Plugin Unoffical, some of this code is by the original developers, however was improved by myself.
# Didn't think it was necessary to re-make something that works well.
import bpy
from typing import Dict, List, Optional, Tuple, Any, Set
from typing import Dict, List, Optional, Tuple, Any, Set, Union
from bpy.types import Operator, Context, Object, ShapeKey
from collections import OrderedDict
from ..core.logging_setup import logger
@@ -16,22 +15,24 @@ from ..core.common import (
)
class VisemeCache:
"""Caches generated viseme shape data"""
_cache: Dict = {}
"""Manages caching of generated viseme shape data for performance optimization"""
_cache: Dict[Tuple[str, Tuple[Tuple]], List] = {}
@classmethod
def get_cached_shape(cls, key: str, mix_data: List) -> Optional[List]:
def get_cached_shape(cls, key: str, mix_data: List[List[Union[str, float]]]) -> Optional[List]:
"""Retrieves cached shape data for a given viseme key and mix configuration"""
cache_key = (key, tuple(tuple(x) for x in mix_data))
return cls._cache.get(cache_key)
@classmethod
def cache_shape(cls, key: str, mix_data: List, shape_data: List) -> None:
def cache_shape(cls, key: str, mix_data: List[List[Union[str, float]]], shape_data: List) -> None:
"""Stores shape data in cache for future retrieval"""
cache_key = (key, tuple(tuple(x) for x in mix_data))
cls._cache[cache_key] = shape_data
class VisemePreview:
"""Handles viseme preview functionality"""
_preview_data: Dict = {}
"""Controls real-time preview functionality for viseme shapes"""
_preview_data: Dict[str, float] = {}
_active: bool = False
_preview_shapes: Optional[OrderedDict] = None
@@ -117,13 +118,18 @@ class VisemePreview:
cls._preview_shapes = None
class ATOOLKIT_OT_preview_visemes(Operator):
bl_idname = "avatar_toolkit.preview_visemes"
bl_label = t("Visemes.preview_label")
bl_description = t("Visemes.preview_desc")
bl_options = {'REGISTER', 'UNDO', 'INTERNAL'}
"""Operator for previewing viseme shapes in real-time"""
bl_idname: str = "avatar_toolkit.preview_visemes"
bl_label: str = t("Visemes.preview_label")
bl_description: str = t("Visemes.preview_desc")
bl_options: Set[str] = {'REGISTER', 'UNDO', 'INTERNAL'}
@classmethod
def poll(cls, context: Context) -> bool:
# Check if we're in object mode first
if context.mode != 'OBJECT':
return False
armature = get_active_armature(context)
if not armature:
return False
@@ -165,10 +171,11 @@ def validate_deformation(mesh, mix_data):
return max_deform < (mesh_size * 0.4)
class ATOOLKIT_OT_create_visemes(Operator):
bl_idname = "avatar_toolkit.create_visemes"
bl_label = t("Visemes.create_label")
bl_description = t("Visemes.create_desc")
bl_options = {'REGISTER', 'UNDO'}
"""Operator for generating VRChat-compatible viseme shape keys"""
bl_idname: str = "avatar_toolkit.create_visemes"
bl_label: str = t("Visemes.create_label")
bl_description: str = t("Visemes.create_desc")
bl_options: Set[str] = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context: Context) -> bool: