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