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:
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user