Housekeeping (bug fixes)

NEW FEATURES:
- added apply shapekey to basis from Cats
  - now that pesky thing I keep going back to cats for is in Avatar Toolkit.

BUG FIXES:
- now we push armature santizers into functions where they are needed
  - this prevents the methods from mirroring changes while working, causing them to blow up when mirror mode is on
  - more changes to come for armature setting santitizers
- fixed error reporting
  - now methods when catching errors will return full error tracebacks
  - this will help make debugging and finding user issues easier.
This commit is contained in:
989onan
2025-07-10 18:44:42 -04:00
parent 89fc8bc9c8
commit 6d9f751a16
27 changed files with 663 additions and 143 deletions
+2 -1
View File
@@ -8,6 +8,7 @@ from ..core.common import SceneMatClass, MaterialListBool, ProgressTracker
from ..core.packer.rectangle_packer import MaterialImageList, BinPacker
from ..core.translations import t
from ..core.logging_setup import logger
import traceback
class MaterialImageList:
def __init__(self):
@@ -306,6 +307,6 @@ class AvatarToolKit_OT_AtlasMaterials(Operator):
return {"FINISHED"}
except Exception as e:
logger.error(f"Error creating material atlas: {str(e)}", exc_info=True)
logger.error(f"Error creating material atlas: {traceback.format_exc()}", exc_info=True)
self.report({'ERROR'}, t("TextureAtlas.atlas_error"))
raise e
View File
@@ -13,6 +13,8 @@ from ...core.common import (
join_mesh_objects,
remove_unused_shapekeys,
identify_bones,
store_breaking_settings_armature,
restore_breaking_settings_armature,
)
from ...core.dictionaries import simplify_bonename
@@ -42,6 +44,9 @@ class AvatarToolkit_OT_MergeArmature(bpy.types.Operator):
logger.error(f"Armature not found: {merge_armature_name}")
self.report({'ERROR'}, t('MergeArmature.error.not_found', name=merge_armature_name))
return {'CANCELLED'}
#Store current armature settings that can mess us up.
data_breaking_base = store_breaking_settings_armature(base_armature)
data_breaking_merge = store_breaking_settings_armature(merge_armature)
# Remove Rigid Bodies and Joints
delete_rigidbodies_and_joints(base_armature)
@@ -70,7 +75,11 @@ class AvatarToolkit_OT_MergeArmature(bpy.types.Operator):
wm.progress_update(100)
wm.progress_end()
restore_breaking_settings_armature(base_armature, data_breaking_base)
restore_breaking_settings_armature(merge_armature, data_breaking_merge)
self.report({'INFO'}, t('MergeArmature.success'))
return {'FINISHED'}
except Exception as e:
@@ -117,14 +117,11 @@ class AvatarToolkit_OT_ApplyModifierForShapkeyObj(bpy.types.Operator):
obj.select_set(True)
context.view_layer.objects.active = obj
bpy.ops.object.join_shapes()
except Exception as e:
except Exception:
self.report({'ERROR'}, f"Shapekey joining failed!!")
print(f"Shapekey joining failed!!")
print(traceback.format_exc(e))
#clean up after critical failure
for shape in shapes:
bpy.data.objects.remove(shape)#faster than ops delete
print(traceback.format_exc())
#final clean up
for shape in shapes:
@@ -136,4 +133,6 @@ class AvatarToolkit_OT_ApplyModifierForShapkeyObj(bpy.types.Operator):
return {'FINISHED'}
return {'FINISHED'}
+10 -6
View File
@@ -2,6 +2,7 @@ import bpy
from bpy.types import Operator, Context, Object, ArmatureModifier, VertexGroup
from mathutils import Vector
from typing import Set, Optional, List, Any
import traceback
from ...core.logging_setup import logger
from ...core.translations import t
@@ -10,7 +11,9 @@ from ...core.common import (
get_all_meshes,
ProgressTracker,
calculate_bone_orientation,
add_armature_modifier
add_armature_modifier,
store_breaking_settings_armature,
restore_breaking_settings_armature,
)
from ...core.armature_validation import validate_armature
@@ -83,11 +86,11 @@ class AvatarToolkit_OT_AttachMesh(Operator):
attach_to_bone = armature.data.edit_bones.get(attach_bone_name)
if not attach_to_bone:
raise ValueError(t("AttachMesh.error.bone_not_found", bone=attach_bone_name))
data_breaking = store_breaking_settings_armature(armature)
mesh_bone = armature.data.edit_bones.new(mesh_name)
mesh_bone.parent = attach_to_bone
progress.step(t("AttachMesh.create_bone"))
# Calculate bone placement
verts_in_group: List[Any] = [v for v in mesh.data.vertices
for g in v.groups if g.group == vg.index]
@@ -104,6 +107,7 @@ class AvatarToolkit_OT_AttachMesh(Operator):
mesh_bone.head = center
mesh_bone.tail = center + Vector((0, 0, max(0.1, dimensions.z)))
mesh_bone.roll = roll_angle
restore_breaking_settings_armature(armature, data_breaking)
progress.step(t("AttachMesh.position_bone"))
bpy.ops.object.mode_set(mode='OBJECT')
@@ -114,9 +118,9 @@ class AvatarToolkit_OT_AttachMesh(Operator):
self.report({'INFO'}, t("AttachMesh.success"))
return {'FINISHED'}
except Exception as e:
logger.error(f"Failed to attach mesh: {str(e)}")
self.report({'ERROR'}, str(e))
except Exception:
logger.error(f"Failed to attach mesh: {traceback.format_exc()}")
self.report({'ERROR'}, traceback.format_exc())
return {'CANCELLED'}
def validate_mesh_transforms(mesh: Optional[Object]) -> tuple[bool, str]:
+8 -7
View File
@@ -10,6 +10,7 @@ from typing import Optional, Dict, Tuple, Set, List, Any, Union, ClassVar
from collections import OrderedDict
from random import random
from itertools import chain
import traceback
from ..core.logging_setup import logger
from ..core.translations import t
@@ -104,8 +105,8 @@ class CreateEyesAV3Button(bpy.types.Operator):
self.report({'INFO'}, t('EyeTracking.success'))
return {'FINISHED'}
except Exception as e:
logger.error(f"Eye tracking setup failed: {str(e)}")
except Exception:
logger.error(f"Eye tracking setup failed: {traceback.format_exc()}")
return {'CANCELLED'}
class CreateEyesSDK2Button(bpy.types.Operator):
@@ -197,7 +198,7 @@ class CreateEyesSDK2Button(bpy.types.Operator):
return {'FINISHED'}
except Exception as e:
logger.error(f"Eye tracking setup failed: {str(e)}")
logger.error(f"Eye tracking setup failed: {traceback.format_exc()}")
return {'CANCELLED'}
class EyeTrackingBackup:
@@ -222,8 +223,8 @@ class EyeTrackingBackup:
with open(self.backup_path, 'w') as f:
json.dump(self.bone_positions, f)
return True
except Exception as e:
logger.error(f"Backup failed: {str(e)}")
except Exception:
logger.error(f"Backup failed: {traceback.format_exc()}")
return False
def restore_bone_positions(self, armature) -> bool:
@@ -243,8 +244,8 @@ class EyeTrackingBackup:
bone.tail = positions['tail']
return True
except Exception as e:
logger.error(f"Restore failed: {str(e)}")
except Exception:
logger.error(f"Restore failed: {traceback.format_exc()}")
return False
class EyeTrackingValidator:
+10 -9
View File
@@ -18,6 +18,7 @@ from ...core.common import (
ProgressTracker
)
from ...core.armature_validation import validate_armature
import traceback
def textures_match(tex1: ShaderNodeTexImage, tex2: ShaderNodeTexImage) -> bool:
"""Compare two texture nodes for matching properties and image data"""
@@ -112,24 +113,24 @@ class AvatarToolkit_OT_CombineMaterials(Operator):
with ProgressTracker(context, 4, "Combining Materials") as progress:
try:
num_combined = self.consolidate_materials(meshes)
except Exception as e:
logger.error(f"Material consolidation failed: {str(e)}")
except Exception:
logger.error(f"Material consolidation failed: {traceback.format_exc()}")
self.report({'ERROR'}, t("Optimization.error.consolidation"))
return {'CANCELLED'}
progress.step("Consolidated materials")
try:
num_cleaned = self.clean_material_slots(meshes)
except Exception as e:
logger.error(f"Material slot cleanup failed: {str(e)}")
except Exception:
logger.error(f"Material slot cleanup failed: {traceback.format_exc()}")
self.report({'ERROR'}, t("Optimization.error.slot_cleanup"))
return {'CANCELLED'}
progress.step("Cleaned material slots")
try:
num_removed = clear_unused_data_blocks()
except Exception as e:
logger.error(f"Data block cleanup failed: {str(e)}")
except Exception:
logger.error(f"Data block cleanup failed: {traceback.format_exc()}")
self.report({'ERROR'}, t("Optimization.error.data_cleanup"))
return {'CANCELLED'}
progress.step("Removed unused data blocks")
@@ -141,9 +142,9 @@ class AvatarToolkit_OT_CombineMaterials(Operator):
return {'FINISHED'}
except Exception as e:
logger.error(f"Failed to combine materials: {str(e)}")
self.report({'ERROR'}, t("Optimization.error.combine_materials", error=str(e)))
except Exception:
logger.error(f"Failed to combine materials: {traceback.format_exc()}")
self.report({'ERROR'}, t("Optimization.error.combine_materials", error=traceback.format_exc()))
return {'CANCELLED'}
def consolidate_materials(self, meshes: List[Object]) -> int:
+7 -6
View File
@@ -11,6 +11,7 @@ from ...core.common import (
ProgressTracker
)
from ...core.armature_validation import validate_armature
import traceback
class AvatarToolkit_OT_JoinAllMeshes(Operator):
"""Operator to join all meshes in the scene"""
@@ -51,9 +52,9 @@ class AvatarToolkit_OT_JoinAllMeshes(Operator):
self.report({'ERROR'}, t("Optimization.error.join_meshes"))
return {'CANCELLED'}
except Exception as e:
logger.error(f"Failed to join meshes: {str(e)}")
self.report({'ERROR'}, t("Optimization.error.join_meshes", error=str(e)))
except Exception:
logger.error(f"Failed to join meshes: {traceback.format_exc()}")
self.report({'ERROR'}, t("Optimization.error.join_meshes", error=traceback.format_exc()))
return {'CANCELLED'}
class AvatarToolkit_OT_JoinSelectedMeshes(Operator):
@@ -95,7 +96,7 @@ class AvatarToolkit_OT_JoinSelectedMeshes(Operator):
self.report({'ERROR'}, t("Optimization.error.join_selected"))
return {'CANCELLED'}
except Exception as e:
logger.error(f"Failed to join selected meshes: {str(e)}")
self.report({'ERROR'}, t("Optimization.error.join_selected", error=str(e)))
except Exception:
logger.error(f"Failed to join selected meshes: {traceback.format_exc()}")
self.report({'ERROR'}, t("Optimization.error.join_selected", error=traceback.format_exc()))
return {'CANCELLED'}
+2 -2
View File
@@ -119,8 +119,8 @@ class AvatarToolkit_OT_RemoveDoubles(Operator):
context.window_manager.modal_handler_add(self)
return {'RUNNING_MODAL'}
except Exception as e:
logger.error(f"Error in execute: {str(e)}")
except Exception:
logger.error(f"Error in execute: {traceback.format_exc()}")
return {'CANCELLED'}
def modal(self, context: Context, event: Event) -> set[ModalReturnType]:
+13 -12
View File
@@ -14,6 +14,7 @@ from ..core.common import (
process_armature_modifiers,
ProgressTracker
)
import traceback
from ..core.armature_validation import validate_armature
class BatchPoseOperationMixin:
@@ -62,9 +63,9 @@ class AvatarToolkit_OT_StartPoseMode(Operator):
bpy.ops.object.mode_set(mode='POSE')
return {'FINISHED'}
except Exception as e:
logger.error(f"Failed to start pose mode: {str(e)}")
self.report({'ERROR'}, t("PoseMode.error.start", error=str(e)))
except Exception:
logger.error(f"Failed to start pose mode: {traceback.format_exc()}")
self.report({'ERROR'}, t("PoseMode.error.start", error=traceback.format_exc()))
return {'CANCELLED'}
class AvatarToolkit_OT_StopPoseMode(Operator):
@@ -85,9 +86,9 @@ class AvatarToolkit_OT_StopPoseMode(Operator):
bpy.ops.pose.select_all(action="INVERT")
bpy.ops.object.mode_set(mode='OBJECT')
return {'FINISHED'}
except Exception as e:
logger.error(f"Failed to stop pose mode: {str(e)}")
self.report({'ERROR'}, t("PoseMode.error.stop", error=str(e)))
except Exception:
logger.error(f"Failed to stop pose mode: {traceback.format_exc()}")
self.report({'ERROR'}, t("PoseMode.error.stop", error=traceback.format_exc()))
return {'CANCELLED'}
class AvatarToolkit_OT_ApplyPoseAsRest(Operator, BatchPoseOperationMixin):
@@ -129,9 +130,9 @@ class AvatarToolkit_OT_ApplyPoseAsRest(Operator, BatchPoseOperationMixin):
progress.step(f"Processed {mesh_obj.name}")
return {'FINISHED'}
except Exception as e:
logger.error(f"Failed to apply pose as shape key: {str(e)}")
self.report({'ERROR'}, t("PoseMode.error.shapekey", error=str(e)))
except Exception:
logger.error(f"Failed to apply pose as shape key: {traceback.format_exc()}")
self.report({'ERROR'}, t("PoseMode.error.shapekey", error=traceback.format_exc()))
return {'CANCELLED'}
class AvatarToolkit_OT_ApplyPoseAsShapekey(Operator, BatchPoseOperationMixin):
@@ -160,7 +161,7 @@ class AvatarToolkit_OT_ApplyPoseAsShapekey(Operator, BatchPoseOperationMixin):
logger.info("Successfully applied pose as rest")
return {'FINISHED'}
except Exception as e:
logger.error(f"Failed to apply pose as rest: {str(e)}")
self.report({'ERROR'}, t("PoseMode.error.rest_pose", error=str(e)))
except Exception:
logger.error(f"Failed to apply pose as rest: {traceback.format_exc()}")
self.report({'ERROR'}, t("PoseMode.error.rest_pose", error=traceback.format_exc()))
return {'CANCELLED'}
+7 -6
View File
@@ -6,6 +6,7 @@ from ...core.translations import t
from ...core.logging_setup import logger
from ...core.common import get_active_armature, get_all_meshes, remove_unused_shapekeys
from ...core.armature_validation import validate_armature
import traceback
class AvatarToolkit_OT_ApplyTransforms(Operator):
"""Apply all transformations to armature and associated meshes"""
@@ -42,9 +43,9 @@ class AvatarToolkit_OT_ApplyTransforms(Operator):
self.report({'INFO'}, t("Tools.transforms_applied"))
return {'FINISHED'}
except Exception as e:
logger.error(f"Failed to apply transforms: {str(e)}")
self.report({'ERROR'}, str(e))
except Exception:
logger.error(f"Failed to apply transforms: {traceback.format_exc()}")
self.report({'ERROR'}, traceback.format_exc())
return {'CANCELLED'}
class AvatarToolkit_OT_CleanShapekeys(Operator):
@@ -86,7 +87,7 @@ class AvatarToolkit_OT_CleanShapekeys(Operator):
self.report({'INFO'}, t("Tools.shapekeys_removed", count=removed_count))
return {'FINISHED'}
except Exception as e:
logger.error(f"Failed to clean shape keys: {str(e)}")
self.report({'ERROR'}, str(e))
except Exception:
logger.error(f"Failed to clean shape keys: {traceback.format_exc()}")
self.report({'ERROR'}, traceback.format_exc())
return {'CANCELLED'}
+451
View File
@@ -0,0 +1,451 @@
# GPL License
import bpy
import numpy as np
from ...core.translations import t
from typing import Set
class AvatarToolkit_OT_ShapeKeyApplier(bpy.types.Operator):
# Applies the currently active shape key with its current value and vertex group to the 'Basis' shape key and all
# shape keys recursively relative to the 'Basis' shape key.
# Turns the currently active shape key into a shape key that reverts the original application if applied.
bl_idname: str = "avatar_toolkit.shape_key_to_basis"
bl_label: str = t('Tools.shapekey_to_basis.label')
bl_description: str = t('Tools.shapekey_to_basis.desc')
bl_options: Set[str] = {'REGISTER', 'UNDO', 'INTERNAL'}
@classmethod
def poll(cls, context):
# Note that context.object.active_shape_key_index is 0 if there are no shape keys
# So context.object.active_shape_key_index > 0 simultaneously checks that there are shape keys and that the
# active shape key isn't the first one
return (context.mode == 'OBJECT' and
context.object and
# Could be extended to other types that have shape keys, but only MESH supported for now
context.object.type == 'MESH' and
# If the active shape key is the basis, nothing would be done
context.object.active_shape_key_index > 0 and
# If the shapes aren't relative, using relative keys to apply to the basis and all affected keys would
# be wrong and the idea of having a key to revert the change doesn't make sense
context.object.data.shape_keys.use_relative and
# If the active shape key is relative to itself, then it does nothing
context.object.active_shape_key.relative_key != context.object.active_shape_key)
def execute(self, context):
# If an object other than the active object is to be used, it can be specified using a context override
mesh = context.object
# Get shapekey which will be the new basis
new_basis_shapekey = mesh.active_shape_key
# Create a map of key : [keys relative to key]
# Effectively the reverse of the key.relative_key relation
reverse_relative_map = AvatarToolkit_OT_ShapeKeyApplier.ReverseRelativeMap(mesh)
# new_basis_shapekey will only be included if it's relative to itself (new_basis_shapekey cannot be the first shape key as poll() ensures
# that the index of the active shape key is greater than 0)
keys_relative_recursive_to_new_basis = reverse_relative_map.get_relative_recursive_keys(new_basis_shapekey)
# Cancel execution if the new basis shape key is relative to itself (via a loop, since poll already returns false for being immediately relative to itself since that will always do nothing)
# If the relative keys loop back around, then if the key is turned into its reverse after applying, it would affect all keys that it's relative to
# Key1 relative -> Key2
# Key2 relative -> Key1
# If Key1 is applied to Basis, Key1 should be changed to a reverted key in order to undo the application.
# Since Key2 is relative to Key1, it has to be modified to account for the change in Key1 so that its relative movement to Key1 stays the same.
# Since Key1 is relative to Key2, it has to be modified to account for the change in Key2 so that its relative movement to Key2 stays the same, but that creates an infinite loop
#
# Another way of looking at it is if Key1 moves a vertex by +1, then Key2 MUST move that same vertex by -1 since they are relative to each other
# If Key1 is applied to the basis, it should become a reverted key that moves a vertex by -1 instead so that when it's re-applied, it undoes initial application
# But that would mean that Key2 would have to become a key that moves a vertex by +1, and we want the key to keep its original relative movement of -1
if new_basis_shapekey in keys_relative_recursive_to_new_basis:
self.report({'ERROR_INVALID_INPUT'}, t('ShapeKeyApplier.error.recursiveRelativeToLoop', name=new_basis_shapekey.name))
return {'CANCELLED'}
# It should work to pick a different key as a basis, so long as that key is immediately relative to itself (key.relative_key == key)
# On the off chance that old_basis_shapekey is not relative to itself, ReverseRelativeMap(mesh) has special handling that treats it as if it always is
old_basis_shapekey = mesh.data.shape_keys.key_blocks[0]
# old_basis_shapekey will be included if it's relative to itself or if it's the first shape key,
# so it's always included in this case
keys_relative_recursive_to_old_basis = reverse_relative_map.get_relative_recursive_keys(old_basis_shapekey)
# 0.0 would have no effect, so set to 1.0
if new_basis_shapekey.value == 0.0:
new_basis_shapekey.value = 1.0
AvatarToolkit_OT_ShapeKeyApplier.apply_key_to_basis(mesh=mesh,
new_basis_shapekey=new_basis_shapekey,
keys_relative_recursive_to_new_basis=keys_relative_recursive_to_new_basis,
keys_relative_recursive_to_basis=keys_relative_recursive_to_old_basis)
# The active key is now a key that reverts to the old relative key so rename it as such
reverted_string = ' - Reverted'
reverted_string_len = len(reverted_string)
old_name = new_basis_shapekey.name
if new_basis_shapekey.name[-reverted_string_len:] == reverted_string:
# If the last letters of the name are the reverted_string, remove them
new_basis_shapekey.name = new_basis_shapekey.name[:-reverted_string_len]
reverted = True
else:
# Add the reverted_string to the end of the name, so it's clear that this shape key now reverts
new_basis_shapekey.name = new_basis_shapekey.name + reverted_string
reverted = False
# Setting the value to zero will make the mesh appear unchanged in overall shape and help to show that the operator has worked correctly
new_basis_shapekey.value = 0.0
new_basis_shapekey.slider_min = 0.0
# Regardless of what the max was before, 1.0 will now fully undo the applied shape key
new_basis_shapekey.slider_max = 1.0
response_message = 'ShapeKeyApplier.successRemoved' if reverted else 'ShapeKeyApplier.successSet'
self.report({'INFO'}, t(response_message, name=old_name))
return {'FINISHED'}
class ReverseRelativeMap:
def __init__(self, obj):
reverse_relative_map = {}
basis_key = obj.data.shape_keys.key_blocks[0]
for key in obj.data.shape_keys.key_blocks:
# Special handling for basis shape key to treat it as if its always relative to itself
relative_key = basis_key if key == basis_key else key.relative_key
keys_relative_to_relative_key = reverse_relative_map.get(relative_key)
if keys_relative_to_relative_key is None:
keys_relative_to_relative_key = {key}
reverse_relative_map[relative_key] = keys_relative_to_relative_key
else:
keys_relative_to_relative_key.add(key)
self.reverse_relative_map = reverse_relative_map
#
def get_relative_recursive_keys(self, shape_key):
shape_set = set()
# Pretty much a depth-first search, but with loop prevention
def inner_recursive_loop(key, checked_set):
# Prevent infinite loops by maintaining a set of shapes that we've checked
if key not in checked_set:
# Need to add the current key to the set of shapes we've checked before the recursive call
checked_set.add(key)
keys_relative_to_shape_key_inner = self.reverse_relative_map.get(key)
if keys_relative_to_shape_key_inner:
for relative_to_inner in keys_relative_to_shape_key_inner:
shape_set.add(relative_to_inner)
inner_recursive_loop(relative_to_inner, checked_set)
inner_recursive_loop(shape_key, set())
return shape_set
@staticmethod
# Isolate the active shape key such that afterwards, creating a new shape from mix will create a shape key that at
# a value of 1.0 is the same movement as the active shape key at its current value and vertex group
# Returns a function that restores the data that got affected due to the isolation
def isolate_active_shape(obj_with_shapes):
active_shape = obj_with_shapes.active_shape_key
restore_data = {}
# When the value is 1.0, we can simply enable show_only_shape_key on the object
if active_shape.value == 1.0:
if obj_with_shapes.show_only_shape_key:
# Don't need to do anything, it's already isolated
pass
else:
# Store the current .show_only_shape_key value, so it can be restored later
restore_data['show_only_shape_key'] = False
obj_with_shapes.show_only_shape_key = True
# When the value is not 1.0, the next simplest method is to mute all the other shapes on the object
else:
# Mute all shapes and save their current .mute value, so it can be restored later
shapekey_mutes = []
for key_block in obj_with_shapes.data.shape_keys.key_blocks:
shapekey_mutes.append(key_block.mute)
key_block.mute = True
# Unmute the active shape key
active_shape.mute = False
restore_data['mutes'] = shapekey_mutes
# show_only_shape_key acts as if active_shape.value is always 1.0, so it needs to be disabled if it's enabled
if obj_with_shapes.show_only_shape_key:
# store the current value so it can be restored
restore_data['show_only_shape_key'] = True
obj_with_shapes.show_only_shape_key = False
# closure to restore
def restore_function():
if restore_data:
mutes = restore_data.get('mutes')
if mutes:
# Restore shape key mutes
for mute, shape in zip(mutes, obj_with_shapes.data.shape_keys.key_blocks):
shape.mute = mute
show_only_shape_key = restore_data.get('show_only_shape_key')
# show_only_shape_key can be False so need to explicitly check for None
if show_only_shape_key is not None:
# Restore show_only_shape_key
obj_with_shapes.show_only_shape_key = show_only_shape_key
return restore_function
# Figures out what needs to be added to each affected key, then iterates through all the affected keys, getting the current shape,
# adding the corresponding amount to it and then setting that as the new shape.
# Gets and sets shape key positions manually with foreach_get and foreach_set
# The slowest part of this function when the number of vertices increase are the shape_key.data.foreach_set() and
# shape_key.data.foreach_get() calls, so the number of calls of those should be minimised for performance
@staticmethod
def apply_key_to_basis(*, mesh, new_basis_shapekey, keys_relative_recursive_to_new_basis, keys_relative_recursive_to_basis):
data = mesh.data
num_verts = len(data.vertices)
new_basis_shapekey_vertex_group_name = new_basis_shapekey.vertex_group
if new_basis_shapekey_vertex_group_name:
new_basis_shapekey_vertex_group = mesh.vertex_groups.get(new_basis_shapekey_vertex_group_name)
else:
new_basis_shapekey_vertex_group = None
new_basis_affected_by_own_application = new_basis_shapekey in keys_relative_recursive_to_basis
# Array of Vector type is flattened by foreach_get into a sequence so the length needs to be multiplied by 3
flattened_co_length = num_verts * 3
# Store shape key vertex positions for new_basis
# There's no need to initialise the elements to anything since they will all be overwritten
# The ShapeKeyPoint type's 'co' property is a FloatProperty type, these are single precision floats
# It's extremely important for performance that the correct float type (np.single/np.float32) is used
# Using the wrong type could result in 3-5 times slower performance (depending on array length) due to Blender
# being required to iterate through each element in the data first instead of immediately setting/getting all
# the data directly
# See foreach_getset in bpy.rna.c of the Blender source for the implementation
new_basis_co_flat = np.empty(flattened_co_length, dtype=np.single)
new_basis_relative_co_flat = np.empty(flattened_co_length, dtype=np.single)
new_basis_shapekey.data.foreach_get('co', new_basis_co_flat)
new_basis_shapekey.relative_key.data.foreach_get('co', new_basis_relative_co_flat)
# This is movement of the active shape key at a value of 1.0
difference_co_flat = np.subtract(new_basis_co_flat, new_basis_relative_co_flat)
# Scale the difference based on the value of the active key
difference_co_flat_value_scaled = np.multiply(difference_co_flat, new_basis_shapekey.value)
# We can reuse these arrays over and over instead of creating new ones each time
temp_co_array = np.empty(flattened_co_length, dtype=np.single)
temp_co_array2 = np.empty(flattened_co_length, dtype=np.single)
# Scale the difference based on the vertex group of the active key
# Ideally, we would scale difference_co_flat by the weight of each vertex in new_basis_shapekey.vertex_group.
# Unfortunately, Blender has no efficient way to get all the weights for a particular vertex group, so it's
# pretty much always a few times faster to create a new shape from mix and get its 'co' with foreach_get(...)
# https://developer.blender.org/D6227 has the sort of function we're after, which could make it into Blender
# one day.
#
# For reference, the ways to get all vertex weights that you can find on stackoverflow:
# Weights from vertices:
# This scales really poorly when lots of vertices are in multiple vertex groups, especially when the vertices are not in the vertex group we want to check,
# because for every vertex v, v.groups has to be iterated until either the vertex group is found or iteration finishes without finding the vertex group
# vertex_weights = [next((g.weight for g in v.groups if g.group == vertex_group_index), 0) for v in data.vertices]
# Equivalent to:
# vertex_weights = []
# for v in data.vertices:
# weight = 0
# for g in v.groups:
# if g.group == vertex_group_index:
# weight = g.weight
# break
# vertex_weights.append(weight)
#
# Weights from vertex group:
# This doesn't scale poorly with lots of vertex groups like the other way does, but, if most of the vertices aren't in the vertex group, relying on catching
# the exception is really slow. If Blender had a similar method that returned a default value or even just None instead of throwing an exception, this would
# be much faster, though likely still slower than creating a new key from mix.
# Ideally we'd want a fast access method like foreach_get(...) instead of having to iterate through all the vertices individually
# vertex_weights = []
# for i in range(num_verts):
# try:
# weight = vertex_group.weight(i)
# except:
# weight = 0
# vertex_weights.append(weight)
if new_basis_shapekey_vertex_group:
# Need to isolate the active shape key, so that when a new shape is created from mix, it's only the active shape key
restore_function = AvatarToolkit_OT_ShapeKeyApplier.isolate_active_shape(mesh)
# This new shape key has the effect of new_basis.value and new_basis.vertex_group applied
new_basis_mixed = mesh.shape_key_add(name="temp shape (you shouldn't see this)", from_mix=True)
# Restore whatever got changed in order to isolate the active shape key
restore_function()
# Use the temp array, new name for convenience
temp_shape_co_flat = temp_co_array
new_basis_mixed.data.foreach_get('co', temp_shape_co_flat)
# Often, the relative keys are the same, e.g. they're both the 'basis', but if they're not we'll need to get its data
if new_basis_mixed.relative_key == new_basis_shapekey.relative_key:
temp_shape_relative_co_flat = new_basis_relative_co_flat
else:
new_basis_mixed.relative_key.data.foreach_get('co', temp_co_array2)
temp_shape_relative_co_flat = temp_co_array2
difference_co_flat_scaled = np.subtract(temp_shape_co_flat, temp_shape_relative_co_flat)
# Remove new_basis_mixed
active_index = mesh.active_shape_key_index
mesh.shape_key_remove(new_basis_mixed)
mesh.active_shape_key_index = active_index
else:
difference_co_flat_scaled = difference_co_flat_value_scaled
if new_basis_affected_by_own_application:
# All keys in keys_recursive_relative_to_new_basis must also be in keys_recursive_relative_to_basis
# All the keys that will have only difference_co_flat_scaled added to them are those which are neither
# new_basis nor relative recursive to new_basis
keys_not_relative_recursive_to_new_basis_and_not_new_basis = (keys_relative_recursive_to_basis - keys_relative_recursive_to_new_basis) - {new_basis_shapekey}
# This for loop is where most of the execution will happen for 'normal' setups of lots of shape keys relative to the first shape
# I looked into using multiprocessing to parallelise this, but type(key_block) and type(key_block.data) can't be pickled,
# i.e. you can't parallelise a list of either of them
#
# Add difference between new_basis_shapekey and new_basis_shapekey.relative_key (scaled according to the value and vertex_group of new_basis_shapekey)
# We already have the co array for new_basis_shapekey.relative_key, so do it separately to save a foreach_get call
new_basis_shapekey.relative_key.data.foreach_set('co', np.add(new_basis_relative_co_flat, difference_co_flat_scaled, out=temp_co_array))
# And now the rest of the shape keys
for key_block in keys_not_relative_recursive_to_new_basis_and_not_new_basis - {new_basis_shapekey.relative_key}:
key_block.data.foreach_get('co', temp_co_array)
key_block.data.foreach_set('co', np.add(temp_co_array, difference_co_flat_scaled, out=temp_co_array))
# Shorthand key:
# NB = new_basis_shapekey
# NB.r = new_basis_shapekey.relative_key
# r(NB) = reverted(new_basis_shapekey)
# r(NB).r = reverted(new_basis_shapekey).relative_key
# NB.v = new_basis_shapekey.value
# NB.vg = new_basis_shapekey.vertex_group
#
# We need the difference between r(NB) and r(NB).r to be the negative of
# (r(NB) - r(NB).r) * NB.vg = -((NB - NB.r) * NB.v * NB.vg)
# = -(NB - NB.r) * NB.v * NB.vg
# NB.vg cancels on both sides, leaving:
# r(NB) - r(NB).r = -(NB - NB.r) * NB.v
# Rearranging for r(NB) gives:
# r(NB) = r(NB).r - (NB - NB.r) * NB.v
# Note that (NB - NB.r) * NB.v = difference_co_flat_value_scaled so:
# r(NB) = r(NB).r - difference_co_flat_value_scaled
# Note that r(NB).r = NB.r + difference_co_flat_scaled as we've added that to it
# r(NB) = NB.r + difference_co_flat_scaled - difference_co_flat_value_scaled
# Note that r(NB) = NB + X where X is what we want to find to add to NB (and all keys relative to it
# so that their relative differences remain the same)
# NB + X = NB.r + difference_co_flat_scaled - difference_co_flat_value_scaled
# X = NB.r - NB + difference_co_flat_scaled - difference_co_flat_value_scaled
# X = -(NB - NB.r) + difference_co_flat_scaled - difference_co_flat_value_scaled
# Fully expanding out would give:
# X = -(NB - NB.r) + (NB - NB.r) * NB.v * NB.vg - (NB - NB.r) * NB.v
#
# In the case of there being a vertex group, it's too costly to calculate NB.vg on its own, so we'll leave it at
# X = -(NB - NB.r) + difference_co_flat_scaled - (NB - NB.r) * NB.v
# Which we can either factor to
# X = (NB - NB.r)(-1 - NB.v) + difference_co_flat_scaled
# X = difference_co_flat * (-1 - NB.v) + difference_co_flat_scaled
# Or, as NB - NB.r = difference_co_flat, calculate as
# X = -difference_co_flat + difference_co_flat_scaled - difference_co_flat_value_scaled
#
# The numpy functions take close to a negligible amount of the total function time, so the choice isn't very
# important, however, from my own benchmarks, np.multiply(array1, scalar, out=output_array) starts to scale
# slightly better than np.add(array1, array2, out=output_array) once array1 gets to around 9000 elements or
# more
# I guess this is due to the fact that the add operation needs to do 1 extra array access per element, and
# that eventually surpasses the effect of the multiply operation being more expensive than the add
# operation
# In this case, the array length is 3*num_verts, meaning the multiplication option gets better at around
# 3000 vertices. We'll use the multiplication option
if new_basis_shapekey_vertex_group:
np.multiply(difference_co_flat, -1 - new_basis_shapekey.value, out=temp_co_array2)
np.add(temp_co_array2, difference_co_flat_scaled, out=temp_co_array2)
# We already have the co array for new_basis_shapekey, so we can do it separately from the others to
# save a foreach_get call
new_basis_shapekey.data.foreach_set('co', np.add(new_basis_co_flat, temp_co_array2, out=temp_co_array))
# Now add to the rest of the keys
for key_block in keys_relative_recursive_to_new_basis:
key_block.data.foreach_get('co', temp_co_array)
key_block.data.foreach_set('co', np.add(temp_co_array, temp_co_array2, out=temp_co_array))
# But for there not being a vertex group, the NB.vg term can be eliminated as it becomes effectively 1.0
# X = -(NB - NB.r) + (NB - NB.r) * NB.v - (NB - NB.r) * NB.v
# Then the last part cancels out
# X = -(NB - NB.r)
# Giving X = -difference_co_flat
else:
# Instead of adding the difference_co_flat_scaled to each key it will be subtracted from each key instead
# We already have the co array for new_basis_shapekey, so we can do it separately to avoid a foreach_get
# Note that
# difference_co_flat = NB - NB.r
# Rearrange for NB.r
# NB.r = NB - difference_co_flat
# Instead of doing np.subtract(new_basis_co_flat, difference_co_flat) we can simply set NB to NB.r
new_basis_shapekey.data.foreach_set('co', new_basis_relative_co_flat)
# And the rest of the shape keys
for key_block in keys_relative_recursive_to_new_basis:
key_block.data.foreach_get('co', temp_co_array)
key_block.data.foreach_set('co', np.subtract(temp_co_array, difference_co_flat, out=temp_co_array))
else:
# New basis isn't relative to Basis so keys New basis is recursively relative to will remain unchanged
# Keys recursively relative to Basis and Keys recursively relative to new basis will be mutually exclusive
# Typical user setups have all the shape keys immediately relative to Basis, so this won't be used much
# Add the difference between new_basis_shapekey and new_basis_shapekey.relative_key (scaled according to the
# value and vertex_group of new_basis_shapekey)
for key_block in keys_relative_recursive_to_basis:
key_block.data.foreach_get('co', temp_co_array)
key_block.data.foreach_set('co', np.add(temp_co_array, difference_co_flat_scaled, out=temp_co_array))
# The difference between the reverted key and its relative key needs to equal the negative of the
# difference between new_basis and new_basis.relative_key multiplied
# new_basis.vertex_group should be present on both
# (r(NB) - r(NB).r) * NB.vg = -((NB - NB.r) * NB.v * NB.vg)
# = -(NB - NB.r) * NB.v * NB.vg
# NB.vg cancels on both sides, leaving:
# r(NB) - r(NB).r = -(NB - NB.r) * NB.v
# r(NB).r is unchanged, meaning r(NB).r = NB.r
# r(NB) - NB.r = -(NB - NB.r) * NB.v
# r(NB) = X + NB where X is what we want to find to add
# X + NB - NB.r = -(NB - NB.r) * NB.v
# Rearrange for X
# X = -(NB - NB.r) - (NB - NB.r) * NB.v
#
# (NB - NB.r) can be factorised
# X = (NB - NB.r)(-1 - NB.v)
# Note that (NB - NB.r) is difference_co_flat, giving
# X = difference_co_flat * (-1 - NB.v)
#
# Alternatively, instead of factorising, note that (NB - NB.r) * NB.v is difference_co_flat_value_scaled
# X = -(NB - NB.r) - difference_co_flat_value_scaled
# Note that (NB - NB.r) is difference_co_flat, giving
# X = -difference_co_flat - difference_co_flat_value_scaled
# Or
# X = -(difference_co_flat + difference_co_flat_value_scaled)
#
# Since NB.vg isn't present, it doesn't matter whether new_basis_shapekey has a vertex_group or not
#
# As with before, we'll use the multiplication option due to it scaling slightly better with a larger
# number of vertices
# X = difference_co_flat * (-1 - NB.v)
np.multiply(difference_co_flat, -1 - new_basis_shapekey.value, out=temp_co_array2)
# We already have the co array for new_basis_shapekey, so we can do it separately from the others to
# save a foreach_get call
new_basis_shapekey.data.foreach_set('co', np.add(new_basis_co_flat, temp_co_array2, out=temp_co_array))
# And now the rest of the shape keys
for key_block in keys_relative_recursive_to_new_basis:
key_block.data.foreach_get('co', temp_co_array)
key_block.data.foreach_set('co', np.add(temp_co_array, temp_co_array2, out=temp_co_array))
# Update mesh vertices to avoid basis shape key and mesh vertices being desynced until Edit mode has been
# entered and exited, which can cause odd behaviour when creating shape keys with from_mix=False or when
# removing all shape keys.
data.shape_keys.reference_key.data.foreach_get('co', temp_co_array)
data.vertices.foreach_set('co', temp_co_array)
def add_to_menu(self, context):
self.layout.separator()
self.layout.operator(AvatarToolkit_OT_ShapeKeyApplier.bl_idname, text=t('Tools.shapekey_to_basis.label'), icon="KEY_HLT")
+21 -16
View File
@@ -10,19 +10,13 @@ from ...core.common import (
restore_bone_transforms,
remove_unused_vertex_groups,
identify_bones,
duplicate_bone,
store_breaking_settings_armature,
restore_breaking_settings_armature,
)
import traceback
from ...core.armature_validation import validate_armature, validate_bone_hierarchy
def duplicate_bone(bone: EditBone) -> EditBone:
"""Create a duplicate of the given bone"""
arm = bone.id_data
new_bone = arm.edit_bones.new(bone.name + "_copy")
new_bone.head = bone.head
new_bone.tail = bone.tail
new_bone.roll = bone.roll
new_bone.parent = bone.parent
return new_bone
class AvatarToolKit_OT_CreateDigitigradeLegs(Operator):
"""Operator to convert standard legs to digitigrade setup"""
@@ -39,13 +33,15 @@ class AvatarToolKit_OT_CreateDigitigradeLegs(Operator):
return False
valid, _, _ = validate_armature(armature)
return (valid and
context.mode == 'EDIT_ARMATURE' and
(context.mode == 'EDIT_ARMATURE' or context.mode == 'POSE') and
context.selected_editable_bones is not None and
len(context.selected_editable_bones) == 2)
def process_leg_chain(self, digi0: EditBone) -> bool:
"""Process a single leg bone chain"""
try:
bpy.ops.object.mode_set(mode='EDIT')
# Get bone chain
digi1: EditBone = digi0.children[0]
digi2: EditBone = digi1.children[0]
@@ -83,23 +79,23 @@ class AvatarToolKit_OT_CreateDigitigradeLegs(Operator):
for bone in [digi1, digi2]:
if "<noik>" not in bone.name:
bone.name = bone.name.split('.')[0] + "<noik>"
return True
except Exception as e:
self.report({'ERROR'}, t("Tools.digitigrade_error", error=traceback.format_exc()))
return False
def execute(self, context: Context) -> set[str]:
"""Execute the digitigrade conversion"""
bpy.ops.object.mode_set(mode='EDIT')
data_breaking = store_breaking_settings_armature(context.armature)
with ProgressTracker(context, len(context.selected_editable_bones), t("Tools.digitigrade")) as progress:
for digi0 in context.selected_editable_bones:
progress.step(t("Tools.processing_leg", bone=digi0.name))
if not self.process_leg_chain(digi0):
return {'CANCELLED'}
restore_breaking_settings_armature(context.armature, data_breaking)
self.report({'INFO'}, t("Tools.digitigrade_success"))
return {'FINISHED'}
@@ -125,6 +121,8 @@ class AvatarToolKit_OT_DeleteBoneConstraints(Operator):
armature = get_active_armature(context)
bpy.ops.object.select_all(action='DESELECT')
armature.select_set(True)
data_breaking = store_breaking_settings_armature(armature)
context.view_layer.objects.active = armature
bpy.ops.object.mode_set(mode='POSE')
@@ -135,6 +133,7 @@ class AvatarToolKit_OT_DeleteBoneConstraints(Operator):
constraints_removed += 1
bpy.ops.object.mode_set(mode='OBJECT')
restore_breaking_settings_armature(armature, data_breaking)
self.report({'INFO'}, t("Tools.clean_constraints_success", count=constraints_removed))
return {'FINISHED'}
@@ -187,6 +186,8 @@ class AvatarToolKit_OT_RemoveZeroWeightBones(Operator):
# Store initial transforms
bpy.ops.object.mode_set(mode='EDIT')
initial_transforms: Dict[str, Dict[str, Any]] = {}
data_breaking = store_breaking_settings_armature(armature)
for bone in armature.data.edit_bones:
initial_transforms[bone.name] = {
'head': bone.head.copy(),
@@ -246,7 +247,7 @@ class AvatarToolKit_OT_RemoveZeroWeightBones(Operator):
if context.scene.avatar_toolkit.list_only_mode:
self.populate_bone_list(context, zero_weight_bones)
return {'FINISHED'}
restore_breaking_settings_armature(armature, data_breaking)
self.report({'INFO'}, t("Tools.clean_weights_success", count=removed_count))
return {'FINISHED'}
@@ -276,6 +277,7 @@ class AvatarToolKit_OT_RemoveSelectedBones(Operator):
def execute(self, context: Context) -> set[str]:
armature = get_active_armature(context)
data_breaking = store_breaking_settings_armature(armature)
toolkit = context.scene.avatar_toolkit
selected_bones = [item.name for item in toolkit.zero_weight_bones
@@ -288,7 +290,7 @@ class AvatarToolKit_OT_RemoveSelectedBones(Operator):
bpy.ops.object.mode_set(mode='OBJECT')
toolkit.zero_weight_bones.clear()
restore_breaking_settings_armature(armature, data_breaking)
self.report({'INFO'}, t("Tools.bones_removed", count=len(selected_bones)))
return {'FINISHED'}
@@ -315,7 +317,8 @@ class AvatarToolKit_OT_FlipCurrentKeyFrames(Operator):
def execute(self, context: Context) -> set[str]:
armature = get_active_armature(context)
data_breaking = store_breaking_settings_armature(armature)
armature_data: bpy.types.Armature = armature.data
@@ -380,6 +383,7 @@ class AvatarToolKit_OT_FlipCurrentKeyFrames(Operator):
#if armature.keyframe_insert(data_path=new_path, index=curve.array_index, frame=time):
continue
self.report({'ERROR'}, f"Keyframe insertion for key with data path \"{curve.data_path}\" and frame {time} failed!")
restore_breaking_settings_armature(armature, data_breaking)
return {'FINISHED'}
@@ -397,4 +401,5 @@ class AvatarToolKit_OT_FlipCurrentKeyFrames(Operator):
# restore selection
armature_data.bones.foreach_set("select", selected)
restore_breaking_settings_armature(armature, data_breaking)
return {'FINISHED'}
+32 -15
View File
@@ -4,8 +4,9 @@ from typing import Set, List
from bpy.types import Operator, Context, Armature, EditBone
from ...core.translations import t
from ...core.logging_setup import logger
from ...core.common import get_active_armature, get_all_meshes, get_vertex_weights, transfer_vertex_weights
from ...core.common import get_active_armature, get_all_meshes, get_vertex_weights, transfer_vertex_weights, store_breaking_settings_armature, restore_breaking_settings_armature
from ...core.armature_validation import validate_armature
import traceback
class AvatarToolkit_OT_ConnectBones(Operator):
"""Connect disconnected bones in chain"""
@@ -23,8 +24,12 @@ class AvatarToolkit_OT_ConnectBones(Operator):
return valid
def execute(self, context: Context) -> Set[str]:
armature = get_active_armature(context)
data_breaking = store_breaking_settings_armature(armature)
try:
armature = get_active_armature(context)
logger.info("Starting bone connection operation")
bpy.ops.object.mode_set(mode='EDIT')
@@ -47,12 +52,14 @@ class AvatarToolkit_OT_ConnectBones(Operator):
bones_connected += 1
bpy.ops.object.mode_set(mode='OBJECT')
restore_breaking_settings_armature(armature, data_breaking)
self.report({'INFO'}, t("Tools.connect_bones_success", count=bones_connected))
return {'FINISHED'}
except Exception as e:
logger.error(f"Failed to connect bones: {str(e)}")
self.report({'ERROR'}, str(e))
except Exception:
logger.error(f"Failed to connect bones: {traceback.format_exc()}")
self.report({'ERROR'}, traceback.format_exc())
restore_breaking_settings_armature(armature, data_breaking)
return {'CANCELLED'}
class AvatarToolkit_OT_MergeToActive(Operator):
@@ -67,11 +74,15 @@ class AvatarToolkit_OT_MergeToActive(Operator):
armature = get_active_armature(context)
if not armature:
return False
return context.mode == 'EDIT_ARMATURE' and context.active_bone
return (context.mode == 'EDIT_ARMATURE' or context.mode == 'POSE') and context.active_bone
def execute(self, context: Context) -> Set[str]:
armature = get_active_armature(context)
data_breaking = store_breaking_settings_armature(armature)
try:
armature = get_active_armature(context)
bpy.ops.object.mode_set(mode='EDIT')
active_bone = context.active_bone
selected_bones = [b for b in context.selected_editable_bones if b != active_bone]
@@ -102,11 +113,13 @@ class AvatarToolkit_OT_MergeToActive(Operator):
armature.data.edit_bones.remove(bone)
self.report({'INFO'}, t("Tools.merge_to_active_success", count=len(selected_bones)))
restore_breaking_settings_armature(armature, data_breaking)
return {'FINISHED'}
except Exception as e:
logger.error(f"Failed to merge bones: {str(e)}")
self.report({'ERROR'}, str(e))
except Exception:
logger.error(f"Failed to merge bones: {traceback.format_exc()}")
self.report({'ERROR'}, traceback.format_exc())
restore_breaking_settings_armature(armature, data_breaking)
return {'CANCELLED'}
class AvatarToolkit_OT_MergeToParent(Operator):
@@ -121,11 +134,13 @@ class AvatarToolkit_OT_MergeToParent(Operator):
armature = get_active_armature(context)
if not armature:
return False
return context.mode == 'EDIT_ARMATURE'
return (context.mode == 'EDIT_ARMATURE' or context.mode == 'POSE')
def execute(self, context: Context) -> Set[str]:
armature = get_active_armature(context)
data_breaking = store_breaking_settings_armature(armature)
try:
armature = get_active_armature(context)
bpy.ops.object.mode_set(mode='EDIT')
selected_bones = [b for b in context.selected_editable_bones if b.parent]
if not selected_bones:
@@ -153,10 +168,12 @@ class AvatarToolkit_OT_MergeToParent(Operator):
armature.data.edit_bones.remove(bone)
merged_count += 1
restore_breaking_settings_armature(armature, data_breaking)
self.report({'INFO'}, t("Tools.merge_to_parent_success", count=merged_count))
return {'FINISHED'}
except Exception as e:
logger.error(f"Failed to merge bones: {str(e)}")
self.report({'ERROR'}, str(e))
except Exception:
logger.error(f"Failed to merge bones: {traceback.format_exc()}")
self.report({'ERROR'}, traceback.format_exc())
restore_breaking_settings_armature(armature, data_breaking)
return {'CANCELLED'}
+5 -4
View File
@@ -3,6 +3,7 @@ from bpy.types import Operator, Context
from ...core.translations import t
from ...core.common import get_active_armature
from ...core.armature_validation import validate_armature
import traceback
class AvatarToolKit_OT_SeparateByMaterials(Operator):
"""Operator to separate mesh by materials"""
@@ -32,8 +33,8 @@ class AvatarToolKit_OT_SeparateByMaterials(Operator):
bpy.ops.object.mode_set(mode='OBJECT')
self.report({'INFO'}, t("Tools.separate_materials_success"))
return {'FINISHED'}
except Exception as e:
self.report({'ERROR'}, str(e))
except Exception:
self.report({'ERROR'}, traceback.format_exc())
return {'CANCELLED'}
class AvatarToolKit_OT_SeparateByLooseParts(Operator):
@@ -64,6 +65,6 @@ class AvatarToolKit_OT_SeparateByLooseParts(Operator):
bpy.ops.object.mode_set(mode='OBJECT')
self.report({'INFO'}, t("Tools.separate_loose_success"))
return {'FINISHED'}
except Exception as e:
self.report({'ERROR'}, str(e))
except Exception:
self.report({'ERROR'}, traceback.format_exc())
return {'CANCELLED'}
+4 -3
View File
@@ -6,6 +6,7 @@ from ...core.logging_setup import logger
from ...core.translations import t
from ...core.dictionaries import rigify_unity_names, rigify_basic_unity_names, rigify_unnecessary_bones
from ...core.armature_validation import validate_armature
import traceback
class AvatarToolkit_OT_ConvertRigifyToUnity(Operator):
"""Convert Rigify armature to Unity-compatible format"""
@@ -56,9 +57,9 @@ class AvatarToolkit_OT_ConvertRigifyToUnity(Operator):
self.report({'INFO'}, t("Tools.rigify_converted"))
return {'FINISHED'}
except Exception as e:
logger.error(f"Failed to convert Rigify: {str(e)}", exc_info=True)
self.report({'ERROR'}, str(e))
except Exception:
logger.error(f"Failed to convert Rigify: {traceback.format_exc()}", exc_info=True)
self.report({'ERROR'}, traceback.format_exc())
return {'CANCELLED'}
def cleanup_extra_bones(self, armature: Object) -> None:
+11 -6
View File
@@ -1,3 +1,4 @@
import traceback
import bpy
import math
from typing import Dict, List, Set, Tuple, Optional, Any, Union
@@ -25,7 +26,7 @@ class AvatarToolkit_OT_StandardizeArmature(Operator):
@classmethod
def poll(cls, context: Context) -> bool:
armature: Optional[Object] = get_active_armature(context)
return armature is not None and context.mode in {'OBJECT', 'EDIT_ARMATURE'}
return armature is not None and context.mode in {'OBJECT', 'EDIT_ARMATURE', 'POSE'}
def invoke(self, context: Context, event: Any) -> Set[str]:
logger.debug("Invoking standardize armature dialog")
@@ -99,20 +100,24 @@ class AvatarToolkit_OT_StandardizeArmature(Operator):
if original_mode == 'EDIT_ARMATURE':
bpy.ops.object.mode_set(mode='EDIT')
if original_mode == 'POSE':
bpy.ops.object.mode_set(mode='POSE')
return {'FINISHED'}
except Exception as e:
logger.error(f"Failed to standardize armature: {str(e)}")
self.report({'ERROR'}, str(e))
except Exception:
logger.error(f"Failed to standardize armature: {traceback.format_exc()}")
self.report({'ERROR'}, traceback.format_exc())
try:
if original_mode == 'EDIT_ARMATURE':
bpy.ops.object.mode_set(mode='EDIT')
if original_mode == 'POSE':
bpy.ops.object.mode_set(mode='POSE')
else:
bpy.ops.object.mode_set(mode='OBJECT')
except Exception as restore_error:
logger.error(f"Failed to restore original mode: {str(restore_error)}")
except Exception:
logger.error(f"Failed to restore original mode: {traceback.format_exc()}")
return {'CANCELLED'}
+3 -2
View File
@@ -6,6 +6,7 @@ import numpy as np
import math
from ...core.translations import t
from ...core.logging_setup import logger
import traceback
class GenerateLoopTreeResult(TypedDict):
tree: Dict[str, Set[str]]
@@ -247,8 +248,8 @@ class AvatarToolkit_OT_AlignUVEdgesToTarget(Operator):
logger.info(f"Finished mesh {source} for UV's")
except Exception as e:
logger.error(f"Error processing source {source}: {str(e)}")
except Exception:
logger.error(f"Error processing source {source}: {traceback.format_exc()}")
return {'CANCELLED'}
bpy.ops.object.mode_set(mode=prev_mode)
+4 -3
View File
@@ -11,6 +11,7 @@ from ..core.common import (
get_all_meshes,
validate_mesh_for_pose
)
import traceback
class VisemeCache:
"""Manages caching of generated viseme shape data for performance optimization"""
@@ -211,9 +212,9 @@ class AvatarToolkit_OT_CreateVisemes(Operator):
self.create_visemes(context, mesh)
self.report({'INFO'}, t("Visemes.success"))
return {'FINISHED'}
except Exception as e:
logger.error(f"Error creating visemes: {str(e)}")
self.report({'ERROR'}, str(e))
except Exception:
logger.error(f"Error creating visemes: {traceback.format_exc()}")
self.report({'ERROR'}, traceback.format_exc())
return {'CANCELLED'}
def create_visemes(self, context: Context, mesh: Object) -> None: