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:
@@ -1,6 +1,7 @@
|
|||||||
import bpy
|
import bpy
|
||||||
from bpy.app.handlers import persistent
|
from bpy.app.handlers import persistent
|
||||||
|
|
||||||
|
|
||||||
modules = None
|
modules = None
|
||||||
ordered_classes = None
|
ordered_classes = None
|
||||||
|
|
||||||
@@ -42,6 +43,10 @@ def register():
|
|||||||
log_level = get_preference("log_level", "WARNING")
|
log_level = get_preference("log_level", "WARNING")
|
||||||
configure_logging(get_preference("enable_logging", False), log_level)
|
configure_logging(get_preference("enable_logging", False), log_level)
|
||||||
|
|
||||||
|
#this needs to be done last, or at least after whatever things this uses is imported - @989onan
|
||||||
|
from .functions.tools.apply_shapekey_to_basis import add_to_menu
|
||||||
|
bpy.types.MESH_MT_shape_key_context_menu.append(add_to_menu)
|
||||||
|
|
||||||
print("Registration complete")
|
print("Registration complete")
|
||||||
|
|
||||||
def unregister():
|
def unregister():
|
||||||
|
|||||||
+21
-16
@@ -1,3 +1,4 @@
|
|||||||
|
import traceback
|
||||||
import bpy
|
import bpy
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import threading
|
import threading
|
||||||
@@ -199,9 +200,9 @@ def apply_pose_as_rest(context: Context, armature_obj: Object, meshes: List[Obje
|
|||||||
|
|
||||||
return True, t("Operation.pose_applied")
|
return True, t("Operation.pose_applied")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception:
|
||||||
logger.error(f"Error applying pose as rest: {str(e)}")
|
logger.error(f"Error applying pose as rest: {traceback.format_exc()}")
|
||||||
return False, str(e)
|
return False, traceback.format_exc()
|
||||||
|
|
||||||
def apply_armature_to_mesh(armature_obj: Object, mesh_obj: Object) -> None:
|
def apply_armature_to_mesh(armature_obj: Object, mesh_obj: Object) -> None:
|
||||||
"""Apply armature deformation to mesh"""
|
"""Apply armature deformation to mesh"""
|
||||||
@@ -335,8 +336,8 @@ def join_mesh_objects(context: Context, meshes: List[Object], progress: Optional
|
|||||||
|
|
||||||
return joined_mesh
|
return joined_mesh
|
||||||
|
|
||||||
except Exception as e:
|
except Exception:
|
||||||
logger.error(f"Failed to join meshes: {str(e)}")
|
logger.error(f"Failed to join meshes: {traceback.format_exc()}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@@ -365,8 +366,8 @@ def fix_uv_coordinates(context: Context) -> None:
|
|||||||
|
|
||||||
logger.debug(f"UV Fix - Successfully processed {obj.name}")
|
logger.debug(f"UV Fix - Successfully processed {obj.name}")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception:
|
||||||
logger.warning(f"UV Fix - Skipped processing for {obj.name}: {str(e)}")
|
logger.warning(f"UV Fix - Skipped processing for {obj.name}: {traceback.format_exc()}")
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
bpy.ops.object.mode_set(mode='OBJECT')
|
bpy.ops.object.mode_set(mode='OBJECT')
|
||||||
@@ -488,7 +489,6 @@ def fix_zero_length_bones(armature: Object) -> None:
|
|||||||
"""Fix zero length bones by setting a minimum length"""
|
"""Fix zero length bones by setting a minimum length"""
|
||||||
if not armature:
|
if not armature:
|
||||||
return
|
return
|
||||||
|
|
||||||
bpy.ops.object.mode_set(mode='EDIT')
|
bpy.ops.object.mode_set(mode='EDIT')
|
||||||
for bone in armature.data.edit_bones:
|
for bone in armature.data.edit_bones:
|
||||||
if bone.length < 0.001:
|
if bone.length < 0.001:
|
||||||
@@ -631,6 +631,7 @@ def get_objects() -> bpy.types.BlendData:
|
|||||||
|
|
||||||
def duplicate_bone(bone: EditBone) -> EditBone:
|
def duplicate_bone(bone: EditBone) -> EditBone:
|
||||||
"""Create a duplicate of the given bone"""
|
"""Create a duplicate of the given bone"""
|
||||||
|
|
||||||
new_bone: EditBone = bone.id_data.edit_bones.new(bone.name + "_copy")
|
new_bone: EditBone = bone.id_data.edit_bones.new(bone.name + "_copy")
|
||||||
new_bone.head = bone.head.copy()
|
new_bone.head = bone.head.copy()
|
||||||
new_bone.tail = bone.tail.copy()
|
new_bone.tail = bone.tail.copy()
|
||||||
@@ -642,14 +643,18 @@ def duplicate_bone(bone: EditBone) -> EditBone:
|
|||||||
new_bone.use_deform = bone.use_deform
|
new_bone.use_deform = bone.use_deform
|
||||||
return new_bone
|
return new_bone
|
||||||
|
|
||||||
#Binary tools
|
|
||||||
|
|
||||||
|
class ArmatureData(Tuple[bool,bool]):
|
||||||
|
pass
|
||||||
|
|
||||||
#encoding FrooxEngine/C# types in binary:
|
def store_breaking_settings_armature(armature: bpy.types.Object) -> ArmatureData:
|
||||||
|
armature_data: bpy.types.Armature = armature.data
|
||||||
|
return (armature_data.use_mirror_x, armature.pose.use_mirror_x)
|
||||||
|
|
||||||
|
def restore_breaking_settings_armature(armature: bpy.types.Object, data: ArmatureData) -> None:
|
||||||
|
armature_data: bpy.types.Armature = armature.data
|
||||||
|
armature_data.use_mirror_x, armature.pose.use_mirror_x = data
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ from typing import Optional, Callable, Dict, List, Union, Set
|
|||||||
from ..common import clear_default_objects
|
from ..common import clear_default_objects
|
||||||
from ..translations import t
|
from ..translations import t
|
||||||
from ..mmd.core.pmx.importer import PMXImporter
|
from ..mmd.core.pmx.importer import PMXImporter
|
||||||
|
import traceback
|
||||||
|
|
||||||
# Configure logging
|
# Configure logging
|
||||||
logging.basicConfig(level=logging.INFO)
|
logging.basicConfig(level=logging.INFO)
|
||||||
@@ -84,8 +85,8 @@ def import_multi_files(
|
|||||||
progress_callback(fullpath)
|
progress_callback(fullpath)
|
||||||
progress.update(file["name"])
|
progress.update(file["name"])
|
||||||
|
|
||||||
except Exception as e:
|
except Exception:
|
||||||
logger.error(f"Import failed: {str(e)}", exc_info=True)
|
logger.error(f"Import failed: {traceback.format_exc()}", exc_info=True)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
ImportMethod = Callable[[str, List[Dict[str, str]], str], None]
|
ImportMethod = Callable[[str, List[Dict[str, str]], str], None]
|
||||||
@@ -230,6 +231,6 @@ def import_pmx_file(filepath: str) -> None:
|
|||||||
try:
|
try:
|
||||||
importer.execute(**import_settings)
|
importer.execute(**import_settings)
|
||||||
logger.info(f"Successfully imported PMX file: {filepath}")
|
logger.info(f"Successfully imported PMX file: {filepath}")
|
||||||
except Exception as e:
|
except Exception:
|
||||||
logger.error(f"Failed to import PMX file: {str(e)}", exc_info=True)
|
logger.error(f"Failed to import PMX file: {traceback.format_exc()}", exc_info=True)
|
||||||
raise
|
raise
|
||||||
|
|||||||
+11
-10
@@ -11,6 +11,7 @@ from typing import Optional, List, Tuple, Callable, Any, Union
|
|||||||
import bpy
|
import bpy
|
||||||
from bpy.types import Object, ID, Camera, Context
|
from bpy.types import Object, ID, Camera, Context
|
||||||
from mathutils import Vector, Matrix, Euler
|
from mathutils import Vector, Matrix, Euler
|
||||||
|
import traceback
|
||||||
|
|
||||||
from ..bpyutils import FnContext, Props
|
from ..bpyutils import FnContext, Props
|
||||||
from ....core.logging_setup import logger
|
from ....core.logging_setup import logger
|
||||||
@@ -87,8 +88,8 @@ class FnCamera:
|
|||||||
__add_driver(camera_object.data, "type", "not $is_perspective")
|
__add_driver(camera_object.data, "type", "not $is_perspective")
|
||||||
__add_driver(camera_object.data, "lens", "$sensor_height/tan($angle/2)/2")
|
__add_driver(camera_object.data, "lens", "$sensor_height/tan($angle/2)/2")
|
||||||
logger.debug(f"Successfully added drivers to camera: {camera_object.name}")
|
logger.debug(f"Successfully added drivers to camera: {camera_object.name}")
|
||||||
except Exception as e:
|
except Exception:
|
||||||
logger.error(f"Failed to add drivers to camera {camera_object.name}: {str(e)}")
|
logger.error(f"Failed to add drivers to camera {camera_object.name}: {traceback.format_exc()}")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def remove_drivers(camera_object: Object) -> None:
|
def remove_drivers(camera_object: Object) -> None:
|
||||||
@@ -100,8 +101,8 @@ class FnCamera:
|
|||||||
camera_object.data.driver_remove("ortho_scale")
|
camera_object.data.driver_remove("ortho_scale")
|
||||||
camera_object.data.driver_remove("lens")
|
camera_object.data.driver_remove("lens")
|
||||||
logger.debug(f"Successfully removed drivers from camera: {camera_object.name}")
|
logger.debug(f"Successfully removed drivers from camera: {camera_object.name}")
|
||||||
except Exception as e:
|
except Exception:
|
||||||
logger.error(f"Failed to remove drivers from camera {camera_object.name}: {str(e)}")
|
logger.error(f"Failed to remove drivers from camera {camera_object.name}: {traceback.format_exc()}")
|
||||||
|
|
||||||
|
|
||||||
class MigrationFnCamera:
|
class MigrationFnCamera:
|
||||||
@@ -124,8 +125,8 @@ class MigrationFnCamera:
|
|||||||
FnCamera.remove_drivers(camera_object)
|
FnCamera.remove_drivers(camera_object)
|
||||||
FnCamera.add_drivers(camera_object)
|
FnCamera.add_drivers(camera_object)
|
||||||
updated_count += 1
|
updated_count += 1
|
||||||
except Exception as e:
|
except Exception:
|
||||||
logger.error(f"Failed to update MMD camera {camera_object.name}: {str(e)}")
|
logger.error(f"Failed to update MMD camera {camera_object.name}: {traceback.format_exc()}")
|
||||||
|
|
||||||
logger.info(f"Updated {updated_count} MMD cameras")
|
logger.info(f"Updated {updated_count} MMD cameras")
|
||||||
|
|
||||||
@@ -197,8 +198,8 @@ class MMDCamera:
|
|||||||
|
|
||||||
logger.info(f"Successfully converted {cameraObj.name} to MMD camera")
|
logger.info(f"Successfully converted {cameraObj.name} to MMD camera")
|
||||||
return MMDCamera(empty)
|
return MMDCamera(empty)
|
||||||
except Exception as e:
|
except Exception:
|
||||||
logger.error(f"Failed to convert camera {cameraObj.name} to MMD camera: {str(e)}")
|
logger.error(f"Failed to convert camera {cameraObj.name} to MMD camera: {traceback.format_exc()}")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -305,8 +306,8 @@ class MMDCamera:
|
|||||||
logger.info(f"Successfully created MMD camera animation with {frame_count} frames")
|
logger.info(f"Successfully created MMD camera animation with {frame_count} frames")
|
||||||
return MMDCamera(mmd_cam_root)
|
return MMDCamera(mmd_cam_root)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception:
|
||||||
logger.error(f"Failed to create MMD camera animation: {str(e)}")
|
logger.error(f"Failed to create MMD camera animation: {traceback.format_exc()}")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def object(self) -> Object:
|
def object(self) -> Object:
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ from ..core.exceptions import MaterialNotFoundError
|
|||||||
from ..core.material import FnMaterial
|
from ..core.material import FnMaterial
|
||||||
from ..core.shader import _NodeGroupUtils
|
from ..core.shader import _NodeGroupUtils
|
||||||
from ....core.logging_setup import logger
|
from ....core.logging_setup import logger
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
|
||||||
class ConvertMaterialsForCycles(Operator):
|
class ConvertMaterialsForCycles(Operator):
|
||||||
@@ -50,8 +51,8 @@ class ConvertMaterialsForCycles(Operator):
|
|||||||
def execute(self, context: Context) -> Set[str]:
|
def execute(self, context: Context) -> Set[str]:
|
||||||
try:
|
try:
|
||||||
context.scene.render.engine = "CYCLES"
|
context.scene.render.engine = "CYCLES"
|
||||||
except Exception as e:
|
except Exception:
|
||||||
logger.error(f"Failed to change to Cycles render engine: {str(e)}")
|
logger.error(f"Failed to change to Cycles render engine: {traceback.format_exc()}")
|
||||||
self.report({"ERROR"}, " * Failed to change to Cycles render engine.")
|
self.report({"ERROR"}, " * Failed to change to Cycles render engine.")
|
||||||
return {"CANCELLED"}
|
return {"CANCELLED"}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import bpy_extras
|
|||||||
from numpy import double
|
from numpy import double
|
||||||
from typing import Set, Dict
|
from typing import Set, Dict
|
||||||
import re
|
import re
|
||||||
|
import traceback
|
||||||
|
|
||||||
from .common import get_active_armature, ProgressTracker, identify_bones
|
from .common import get_active_armature, ProgressTracker, identify_bones
|
||||||
from bpy.types import Context, Operator
|
from bpy.types import Context, Operator
|
||||||
@@ -91,16 +92,16 @@ class AvatarToolkit_OT_ConvertResonite(Operator):
|
|||||||
|
|
||||||
progress.step(t("Tools.convert_resonite.processing", name=bone.name))
|
progress.step(t("Tools.convert_resonite.processing", name=bone.name))
|
||||||
|
|
||||||
except Exception as e:
|
except Exception:
|
||||||
logger.error(f"Error during Resonite conversion: {str(e)}")
|
logger.error(f"Error during Resonite conversion: {traceback.format_exc()}")
|
||||||
self.report({'ERROR'}, str(e))
|
self.report({'ERROR'}, traceback.format_exc())
|
||||||
return {'CANCELLED'}
|
return {'CANCELLED'}
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
try:
|
try:
|
||||||
bpy.ops.object.mode_set(mode='OBJECT')
|
bpy.ops.object.mode_set(mode='OBJECT')
|
||||||
except Exception as e:
|
except Exception:
|
||||||
logger.warning(f"Error returning to object mode: {str(e)}")
|
logger.warning(f"Error returning to object mode: {traceback.format_exc()}")
|
||||||
|
|
||||||
if translate_bone_fails > 0:
|
if translate_bone_fails > 0:
|
||||||
logger.info(f"Conversion completed with {translate_bone_fails} untranslated bones")
|
logger.info(f"Conversion completed with {translate_bone_fails} untranslated bones")
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from ..core.common import SceneMatClass, MaterialListBool, ProgressTracker
|
|||||||
from ..core.packer.rectangle_packer import MaterialImageList, BinPacker
|
from ..core.packer.rectangle_packer import MaterialImageList, BinPacker
|
||||||
from ..core.translations import t
|
from ..core.translations import t
|
||||||
from ..core.logging_setup import logger
|
from ..core.logging_setup import logger
|
||||||
|
import traceback
|
||||||
|
|
||||||
class MaterialImageList:
|
class MaterialImageList:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@@ -306,6 +307,6 @@ class AvatarToolKit_OT_AtlasMaterials(Operator):
|
|||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
except Exception as e:
|
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"))
|
self.report({'ERROR'}, t("TextureAtlas.atlas_error"))
|
||||||
raise e
|
raise e
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ from ...core.common import (
|
|||||||
join_mesh_objects,
|
join_mesh_objects,
|
||||||
remove_unused_shapekeys,
|
remove_unused_shapekeys,
|
||||||
identify_bones,
|
identify_bones,
|
||||||
|
store_breaking_settings_armature,
|
||||||
|
restore_breaking_settings_armature,
|
||||||
)
|
)
|
||||||
from ...core.dictionaries import simplify_bonename
|
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}")
|
logger.error(f"Armature not found: {merge_armature_name}")
|
||||||
self.report({'ERROR'}, t('MergeArmature.error.not_found', name=merge_armature_name))
|
self.report({'ERROR'}, t('MergeArmature.error.not_found', name=merge_armature_name))
|
||||||
return {'CANCELLED'}
|
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
|
# Remove Rigid Bodies and Joints
|
||||||
delete_rigidbodies_and_joints(base_armature)
|
delete_rigidbodies_and_joints(base_armature)
|
||||||
@@ -70,7 +75,11 @@ class AvatarToolkit_OT_MergeArmature(bpy.types.Operator):
|
|||||||
wm.progress_update(100)
|
wm.progress_update(100)
|
||||||
wm.progress_end()
|
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'))
|
self.report({'INFO'}, t('MergeArmature.success'))
|
||||||
|
|
||||||
return {'FINISHED'}
|
return {'FINISHED'}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -117,14 +117,11 @@ class AvatarToolkit_OT_ApplyModifierForShapkeyObj(bpy.types.Operator):
|
|||||||
obj.select_set(True)
|
obj.select_set(True)
|
||||||
context.view_layer.objects.active = obj
|
context.view_layer.objects.active = obj
|
||||||
bpy.ops.object.join_shapes()
|
bpy.ops.object.join_shapes()
|
||||||
except Exception as e:
|
except Exception:
|
||||||
|
|
||||||
self.report({'ERROR'}, f"Shapekey joining failed!!")
|
self.report({'ERROR'}, f"Shapekey joining failed!!")
|
||||||
print(f"Shapekey joining failed!!")
|
print(f"Shapekey joining failed!!")
|
||||||
print(traceback.format_exc(e))
|
print(traceback.format_exc())
|
||||||
#clean up after critical failure
|
|
||||||
for shape in shapes:
|
|
||||||
bpy.data.objects.remove(shape)#faster than ops delete
|
|
||||||
|
|
||||||
#final clean up
|
#final clean up
|
||||||
for shape in shapes:
|
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 bpy.types import Operator, Context, Object, ArmatureModifier, VertexGroup
|
||||||
from mathutils import Vector
|
from mathutils import Vector
|
||||||
from typing import Set, Optional, List, Any
|
from typing import Set, Optional, List, Any
|
||||||
|
import traceback
|
||||||
|
|
||||||
from ...core.logging_setup import logger
|
from ...core.logging_setup import logger
|
||||||
from ...core.translations import t
|
from ...core.translations import t
|
||||||
@@ -10,7 +11,9 @@ from ...core.common import (
|
|||||||
get_all_meshes,
|
get_all_meshes,
|
||||||
ProgressTracker,
|
ProgressTracker,
|
||||||
calculate_bone_orientation,
|
calculate_bone_orientation,
|
||||||
add_armature_modifier
|
add_armature_modifier,
|
||||||
|
store_breaking_settings_armature,
|
||||||
|
restore_breaking_settings_armature,
|
||||||
)
|
)
|
||||||
from ...core.armature_validation import validate_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)
|
attach_to_bone = armature.data.edit_bones.get(attach_bone_name)
|
||||||
if not attach_to_bone:
|
if not attach_to_bone:
|
||||||
raise ValueError(t("AttachMesh.error.bone_not_found", bone=attach_bone_name))
|
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 = armature.data.edit_bones.new(mesh_name)
|
||||||
mesh_bone.parent = attach_to_bone
|
mesh_bone.parent = attach_to_bone
|
||||||
progress.step(t("AttachMesh.create_bone"))
|
progress.step(t("AttachMesh.create_bone"))
|
||||||
|
|
||||||
# Calculate bone placement
|
# Calculate bone placement
|
||||||
verts_in_group: List[Any] = [v for v in mesh.data.vertices
|
verts_in_group: List[Any] = [v for v in mesh.data.vertices
|
||||||
for g in v.groups if g.group == vg.index]
|
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.head = center
|
||||||
mesh_bone.tail = center + Vector((0, 0, max(0.1, dimensions.z)))
|
mesh_bone.tail = center + Vector((0, 0, max(0.1, dimensions.z)))
|
||||||
mesh_bone.roll = roll_angle
|
mesh_bone.roll = roll_angle
|
||||||
|
restore_breaking_settings_armature(armature, data_breaking)
|
||||||
progress.step(t("AttachMesh.position_bone"))
|
progress.step(t("AttachMesh.position_bone"))
|
||||||
|
|
||||||
bpy.ops.object.mode_set(mode='OBJECT')
|
bpy.ops.object.mode_set(mode='OBJECT')
|
||||||
@@ -114,9 +118,9 @@ class AvatarToolkit_OT_AttachMesh(Operator):
|
|||||||
self.report({'INFO'}, t("AttachMesh.success"))
|
self.report({'INFO'}, t("AttachMesh.success"))
|
||||||
return {'FINISHED'}
|
return {'FINISHED'}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception:
|
||||||
logger.error(f"Failed to attach mesh: {str(e)}")
|
logger.error(f"Failed to attach mesh: {traceback.format_exc()}")
|
||||||
self.report({'ERROR'}, str(e))
|
self.report({'ERROR'}, traceback.format_exc())
|
||||||
return {'CANCELLED'}
|
return {'CANCELLED'}
|
||||||
|
|
||||||
def validate_mesh_transforms(mesh: Optional[Object]) -> tuple[bool, str]:
|
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 collections import OrderedDict
|
||||||
from random import random
|
from random import random
|
||||||
from itertools import chain
|
from itertools import chain
|
||||||
|
import traceback
|
||||||
|
|
||||||
from ..core.logging_setup import logger
|
from ..core.logging_setup import logger
|
||||||
from ..core.translations import t
|
from ..core.translations import t
|
||||||
@@ -104,8 +105,8 @@ class CreateEyesAV3Button(bpy.types.Operator):
|
|||||||
self.report({'INFO'}, t('EyeTracking.success'))
|
self.report({'INFO'}, t('EyeTracking.success'))
|
||||||
return {'FINISHED'}
|
return {'FINISHED'}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception:
|
||||||
logger.error(f"Eye tracking setup failed: {str(e)}")
|
logger.error(f"Eye tracking setup failed: {traceback.format_exc()}")
|
||||||
return {'CANCELLED'}
|
return {'CANCELLED'}
|
||||||
|
|
||||||
class CreateEyesSDK2Button(bpy.types.Operator):
|
class CreateEyesSDK2Button(bpy.types.Operator):
|
||||||
@@ -197,7 +198,7 @@ class CreateEyesSDK2Button(bpy.types.Operator):
|
|||||||
return {'FINISHED'}
|
return {'FINISHED'}
|
||||||
|
|
||||||
except Exception as e:
|
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'}
|
return {'CANCELLED'}
|
||||||
|
|
||||||
class EyeTrackingBackup:
|
class EyeTrackingBackup:
|
||||||
@@ -222,8 +223,8 @@ class EyeTrackingBackup:
|
|||||||
with open(self.backup_path, 'w') as f:
|
with open(self.backup_path, 'w') as f:
|
||||||
json.dump(self.bone_positions, f)
|
json.dump(self.bone_positions, f)
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception:
|
||||||
logger.error(f"Backup failed: {str(e)}")
|
logger.error(f"Backup failed: {traceback.format_exc()}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def restore_bone_positions(self, armature) -> bool:
|
def restore_bone_positions(self, armature) -> bool:
|
||||||
@@ -243,8 +244,8 @@ class EyeTrackingBackup:
|
|||||||
bone.tail = positions['tail']
|
bone.tail = positions['tail']
|
||||||
|
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception:
|
||||||
logger.error(f"Restore failed: {str(e)}")
|
logger.error(f"Restore failed: {traceback.format_exc()}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
class EyeTrackingValidator:
|
class EyeTrackingValidator:
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ from ...core.common import (
|
|||||||
ProgressTracker
|
ProgressTracker
|
||||||
)
|
)
|
||||||
from ...core.armature_validation import validate_armature
|
from ...core.armature_validation import validate_armature
|
||||||
|
import traceback
|
||||||
|
|
||||||
def textures_match(tex1: ShaderNodeTexImage, tex2: ShaderNodeTexImage) -> bool:
|
def textures_match(tex1: ShaderNodeTexImage, tex2: ShaderNodeTexImage) -> bool:
|
||||||
"""Compare two texture nodes for matching properties and image data"""
|
"""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:
|
with ProgressTracker(context, 4, "Combining Materials") as progress:
|
||||||
try:
|
try:
|
||||||
num_combined = self.consolidate_materials(meshes)
|
num_combined = self.consolidate_materials(meshes)
|
||||||
except Exception as e:
|
except Exception:
|
||||||
logger.error(f"Material consolidation failed: {str(e)}")
|
logger.error(f"Material consolidation failed: {traceback.format_exc()}")
|
||||||
self.report({'ERROR'}, t("Optimization.error.consolidation"))
|
self.report({'ERROR'}, t("Optimization.error.consolidation"))
|
||||||
return {'CANCELLED'}
|
return {'CANCELLED'}
|
||||||
progress.step("Consolidated materials")
|
progress.step("Consolidated materials")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
num_cleaned = self.clean_material_slots(meshes)
|
num_cleaned = self.clean_material_slots(meshes)
|
||||||
except Exception as e:
|
except Exception:
|
||||||
logger.error(f"Material slot cleanup failed: {str(e)}")
|
logger.error(f"Material slot cleanup failed: {traceback.format_exc()}")
|
||||||
self.report({'ERROR'}, t("Optimization.error.slot_cleanup"))
|
self.report({'ERROR'}, t("Optimization.error.slot_cleanup"))
|
||||||
return {'CANCELLED'}
|
return {'CANCELLED'}
|
||||||
progress.step("Cleaned material slots")
|
progress.step("Cleaned material slots")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
num_removed = clear_unused_data_blocks()
|
num_removed = clear_unused_data_blocks()
|
||||||
except Exception as e:
|
except Exception:
|
||||||
logger.error(f"Data block cleanup failed: {str(e)}")
|
logger.error(f"Data block cleanup failed: {traceback.format_exc()}")
|
||||||
self.report({'ERROR'}, t("Optimization.error.data_cleanup"))
|
self.report({'ERROR'}, t("Optimization.error.data_cleanup"))
|
||||||
return {'CANCELLED'}
|
return {'CANCELLED'}
|
||||||
progress.step("Removed unused data blocks")
|
progress.step("Removed unused data blocks")
|
||||||
@@ -141,9 +142,9 @@ class AvatarToolkit_OT_CombineMaterials(Operator):
|
|||||||
|
|
||||||
return {'FINISHED'}
|
return {'FINISHED'}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception:
|
||||||
logger.error(f"Failed to combine materials: {str(e)}")
|
logger.error(f"Failed to combine materials: {traceback.format_exc()}")
|
||||||
self.report({'ERROR'}, t("Optimization.error.combine_materials", error=str(e)))
|
self.report({'ERROR'}, t("Optimization.error.combine_materials", error=traceback.format_exc()))
|
||||||
return {'CANCELLED'}
|
return {'CANCELLED'}
|
||||||
|
|
||||||
def consolidate_materials(self, meshes: List[Object]) -> int:
|
def consolidate_materials(self, meshes: List[Object]) -> int:
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ from ...core.common import (
|
|||||||
ProgressTracker
|
ProgressTracker
|
||||||
)
|
)
|
||||||
from ...core.armature_validation import validate_armature
|
from ...core.armature_validation import validate_armature
|
||||||
|
import traceback
|
||||||
|
|
||||||
class AvatarToolkit_OT_JoinAllMeshes(Operator):
|
class AvatarToolkit_OT_JoinAllMeshes(Operator):
|
||||||
"""Operator to join all meshes in the scene"""
|
"""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"))
|
self.report({'ERROR'}, t("Optimization.error.join_meshes"))
|
||||||
return {'CANCELLED'}
|
return {'CANCELLED'}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception:
|
||||||
logger.error(f"Failed to join meshes: {str(e)}")
|
logger.error(f"Failed to join meshes: {traceback.format_exc()}")
|
||||||
self.report({'ERROR'}, t("Optimization.error.join_meshes", error=str(e)))
|
self.report({'ERROR'}, t("Optimization.error.join_meshes", error=traceback.format_exc()))
|
||||||
return {'CANCELLED'}
|
return {'CANCELLED'}
|
||||||
|
|
||||||
class AvatarToolkit_OT_JoinSelectedMeshes(Operator):
|
class AvatarToolkit_OT_JoinSelectedMeshes(Operator):
|
||||||
@@ -95,7 +96,7 @@ class AvatarToolkit_OT_JoinSelectedMeshes(Operator):
|
|||||||
self.report({'ERROR'}, t("Optimization.error.join_selected"))
|
self.report({'ERROR'}, t("Optimization.error.join_selected"))
|
||||||
return {'CANCELLED'}
|
return {'CANCELLED'}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception:
|
||||||
logger.error(f"Failed to join selected meshes: {str(e)}")
|
logger.error(f"Failed to join selected meshes: {traceback.format_exc()}")
|
||||||
self.report({'ERROR'}, t("Optimization.error.join_selected", error=str(e)))
|
self.report({'ERROR'}, t("Optimization.error.join_selected", error=traceback.format_exc()))
|
||||||
return {'CANCELLED'}
|
return {'CANCELLED'}
|
||||||
|
|||||||
@@ -119,8 +119,8 @@ class AvatarToolkit_OT_RemoveDoubles(Operator):
|
|||||||
context.window_manager.modal_handler_add(self)
|
context.window_manager.modal_handler_add(self)
|
||||||
return {'RUNNING_MODAL'}
|
return {'RUNNING_MODAL'}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception:
|
||||||
logger.error(f"Error in execute: {str(e)}")
|
logger.error(f"Error in execute: {traceback.format_exc()}")
|
||||||
return {'CANCELLED'}
|
return {'CANCELLED'}
|
||||||
|
|
||||||
def modal(self, context: Context, event: Event) -> set[ModalReturnType]:
|
def modal(self, context: Context, event: Event) -> set[ModalReturnType]:
|
||||||
|
|||||||
+13
-12
@@ -14,6 +14,7 @@ from ..core.common import (
|
|||||||
process_armature_modifiers,
|
process_armature_modifiers,
|
||||||
ProgressTracker
|
ProgressTracker
|
||||||
)
|
)
|
||||||
|
import traceback
|
||||||
from ..core.armature_validation import validate_armature
|
from ..core.armature_validation import validate_armature
|
||||||
|
|
||||||
class BatchPoseOperationMixin:
|
class BatchPoseOperationMixin:
|
||||||
@@ -62,9 +63,9 @@ class AvatarToolkit_OT_StartPoseMode(Operator):
|
|||||||
bpy.ops.object.mode_set(mode='POSE')
|
bpy.ops.object.mode_set(mode='POSE')
|
||||||
|
|
||||||
return {'FINISHED'}
|
return {'FINISHED'}
|
||||||
except Exception as e:
|
except Exception:
|
||||||
logger.error(f"Failed to start pose mode: {str(e)}")
|
logger.error(f"Failed to start pose mode: {traceback.format_exc()}")
|
||||||
self.report({'ERROR'}, t("PoseMode.error.start", error=str(e)))
|
self.report({'ERROR'}, t("PoseMode.error.start", error=traceback.format_exc()))
|
||||||
return {'CANCELLED'}
|
return {'CANCELLED'}
|
||||||
|
|
||||||
class AvatarToolkit_OT_StopPoseMode(Operator):
|
class AvatarToolkit_OT_StopPoseMode(Operator):
|
||||||
@@ -85,9 +86,9 @@ class AvatarToolkit_OT_StopPoseMode(Operator):
|
|||||||
bpy.ops.pose.select_all(action="INVERT")
|
bpy.ops.pose.select_all(action="INVERT")
|
||||||
bpy.ops.object.mode_set(mode='OBJECT')
|
bpy.ops.object.mode_set(mode='OBJECT')
|
||||||
return {'FINISHED'}
|
return {'FINISHED'}
|
||||||
except Exception as e:
|
except Exception:
|
||||||
logger.error(f"Failed to stop pose mode: {str(e)}")
|
logger.error(f"Failed to stop pose mode: {traceback.format_exc()}")
|
||||||
self.report({'ERROR'}, t("PoseMode.error.stop", error=str(e)))
|
self.report({'ERROR'}, t("PoseMode.error.stop", error=traceback.format_exc()))
|
||||||
return {'CANCELLED'}
|
return {'CANCELLED'}
|
||||||
|
|
||||||
class AvatarToolkit_OT_ApplyPoseAsRest(Operator, BatchPoseOperationMixin):
|
class AvatarToolkit_OT_ApplyPoseAsRest(Operator, BatchPoseOperationMixin):
|
||||||
@@ -129,9 +130,9 @@ class AvatarToolkit_OT_ApplyPoseAsRest(Operator, BatchPoseOperationMixin):
|
|||||||
progress.step(f"Processed {mesh_obj.name}")
|
progress.step(f"Processed {mesh_obj.name}")
|
||||||
|
|
||||||
return {'FINISHED'}
|
return {'FINISHED'}
|
||||||
except Exception as e:
|
except Exception:
|
||||||
logger.error(f"Failed to apply pose as shape key: {str(e)}")
|
logger.error(f"Failed to apply pose as shape key: {traceback.format_exc()}")
|
||||||
self.report({'ERROR'}, t("PoseMode.error.shapekey", error=str(e)))
|
self.report({'ERROR'}, t("PoseMode.error.shapekey", error=traceback.format_exc()))
|
||||||
return {'CANCELLED'}
|
return {'CANCELLED'}
|
||||||
|
|
||||||
class AvatarToolkit_OT_ApplyPoseAsShapekey(Operator, BatchPoseOperationMixin):
|
class AvatarToolkit_OT_ApplyPoseAsShapekey(Operator, BatchPoseOperationMixin):
|
||||||
@@ -160,7 +161,7 @@ class AvatarToolkit_OT_ApplyPoseAsShapekey(Operator, BatchPoseOperationMixin):
|
|||||||
|
|
||||||
logger.info("Successfully applied pose as rest")
|
logger.info("Successfully applied pose as rest")
|
||||||
return {'FINISHED'}
|
return {'FINISHED'}
|
||||||
except Exception as e:
|
except Exception:
|
||||||
logger.error(f"Failed to apply pose as rest: {str(e)}")
|
logger.error(f"Failed to apply pose as rest: {traceback.format_exc()}")
|
||||||
self.report({'ERROR'}, t("PoseMode.error.rest_pose", error=str(e)))
|
self.report({'ERROR'}, t("PoseMode.error.rest_pose", error=traceback.format_exc()))
|
||||||
return {'CANCELLED'}
|
return {'CANCELLED'}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from ...core.translations import t
|
|||||||
from ...core.logging_setup import logger
|
from ...core.logging_setup import logger
|
||||||
from ...core.common import get_active_armature, get_all_meshes, remove_unused_shapekeys
|
from ...core.common import get_active_armature, get_all_meshes, remove_unused_shapekeys
|
||||||
from ...core.armature_validation import validate_armature
|
from ...core.armature_validation import validate_armature
|
||||||
|
import traceback
|
||||||
|
|
||||||
class AvatarToolkit_OT_ApplyTransforms(Operator):
|
class AvatarToolkit_OT_ApplyTransforms(Operator):
|
||||||
"""Apply all transformations to armature and associated meshes"""
|
"""Apply all transformations to armature and associated meshes"""
|
||||||
@@ -42,9 +43,9 @@ class AvatarToolkit_OT_ApplyTransforms(Operator):
|
|||||||
self.report({'INFO'}, t("Tools.transforms_applied"))
|
self.report({'INFO'}, t("Tools.transforms_applied"))
|
||||||
return {'FINISHED'}
|
return {'FINISHED'}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception:
|
||||||
logger.error(f"Failed to apply transforms: {str(e)}")
|
logger.error(f"Failed to apply transforms: {traceback.format_exc()}")
|
||||||
self.report({'ERROR'}, str(e))
|
self.report({'ERROR'}, traceback.format_exc())
|
||||||
return {'CANCELLED'}
|
return {'CANCELLED'}
|
||||||
|
|
||||||
class AvatarToolkit_OT_CleanShapekeys(Operator):
|
class AvatarToolkit_OT_CleanShapekeys(Operator):
|
||||||
@@ -86,7 +87,7 @@ class AvatarToolkit_OT_CleanShapekeys(Operator):
|
|||||||
self.report({'INFO'}, t("Tools.shapekeys_removed", count=removed_count))
|
self.report({'INFO'}, t("Tools.shapekeys_removed", count=removed_count))
|
||||||
return {'FINISHED'}
|
return {'FINISHED'}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception:
|
||||||
logger.error(f"Failed to clean shape keys: {str(e)}")
|
logger.error(f"Failed to clean shape keys: {traceback.format_exc()}")
|
||||||
self.report({'ERROR'}, str(e))
|
self.report({'ERROR'}, traceback.format_exc())
|
||||||
return {'CANCELLED'}
|
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,
|
restore_bone_transforms,
|
||||||
remove_unused_vertex_groups,
|
remove_unused_vertex_groups,
|
||||||
identify_bones,
|
identify_bones,
|
||||||
|
duplicate_bone,
|
||||||
|
store_breaking_settings_armature,
|
||||||
|
restore_breaking_settings_armature,
|
||||||
)
|
)
|
||||||
import traceback
|
import traceback
|
||||||
from ...core.armature_validation import validate_armature, validate_bone_hierarchy
|
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):
|
class AvatarToolKit_OT_CreateDigitigradeLegs(Operator):
|
||||||
"""Operator to convert standard legs to digitigrade setup"""
|
"""Operator to convert standard legs to digitigrade setup"""
|
||||||
@@ -39,13 +33,15 @@ class AvatarToolKit_OT_CreateDigitigradeLegs(Operator):
|
|||||||
return False
|
return False
|
||||||
valid, _, _ = validate_armature(armature)
|
valid, _, _ = validate_armature(armature)
|
||||||
return (valid and
|
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
|
context.selected_editable_bones is not None and
|
||||||
len(context.selected_editable_bones) == 2)
|
len(context.selected_editable_bones) == 2)
|
||||||
|
|
||||||
def process_leg_chain(self, digi0: EditBone) -> bool:
|
def process_leg_chain(self, digi0: EditBone) -> bool:
|
||||||
"""Process a single leg bone chain"""
|
"""Process a single leg bone chain"""
|
||||||
try:
|
try:
|
||||||
|
bpy.ops.object.mode_set(mode='EDIT')
|
||||||
|
|
||||||
# Get bone chain
|
# Get bone chain
|
||||||
digi1: EditBone = digi0.children[0]
|
digi1: EditBone = digi0.children[0]
|
||||||
digi2: EditBone = digi1.children[0]
|
digi2: EditBone = digi1.children[0]
|
||||||
@@ -83,23 +79,23 @@ class AvatarToolKit_OT_CreateDigitigradeLegs(Operator):
|
|||||||
for bone in [digi1, digi2]:
|
for bone in [digi1, digi2]:
|
||||||
if "<noik>" not in bone.name:
|
if "<noik>" not in bone.name:
|
||||||
bone.name = bone.name.split('.')[0] + "<noik>"
|
bone.name = bone.name.split('.')[0] + "<noik>"
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.report({'ERROR'}, t("Tools.digitigrade_error", error=traceback.format_exc()))
|
self.report({'ERROR'}, t("Tools.digitigrade_error", error=traceback.format_exc()))
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def execute(self, context: Context) -> set[str]:
|
def execute(self, context: Context) -> set[str]:
|
||||||
"""Execute the digitigrade conversion"""
|
"""Execute the digitigrade conversion"""
|
||||||
bpy.ops.object.mode_set(mode='EDIT')
|
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:
|
with ProgressTracker(context, len(context.selected_editable_bones), t("Tools.digitigrade")) as progress:
|
||||||
for digi0 in context.selected_editable_bones:
|
for digi0 in context.selected_editable_bones:
|
||||||
progress.step(t("Tools.processing_leg", bone=digi0.name))
|
progress.step(t("Tools.processing_leg", bone=digi0.name))
|
||||||
if not self.process_leg_chain(digi0):
|
if not self.process_leg_chain(digi0):
|
||||||
return {'CANCELLED'}
|
return {'CANCELLED'}
|
||||||
|
restore_breaking_settings_armature(context.armature, data_breaking)
|
||||||
self.report({'INFO'}, t("Tools.digitigrade_success"))
|
self.report({'INFO'}, t("Tools.digitigrade_success"))
|
||||||
return {'FINISHED'}
|
return {'FINISHED'}
|
||||||
|
|
||||||
@@ -125,6 +121,8 @@ class AvatarToolKit_OT_DeleteBoneConstraints(Operator):
|
|||||||
armature = get_active_armature(context)
|
armature = get_active_armature(context)
|
||||||
bpy.ops.object.select_all(action='DESELECT')
|
bpy.ops.object.select_all(action='DESELECT')
|
||||||
armature.select_set(True)
|
armature.select_set(True)
|
||||||
|
data_breaking = store_breaking_settings_armature(armature)
|
||||||
|
|
||||||
context.view_layer.objects.active = armature
|
context.view_layer.objects.active = armature
|
||||||
bpy.ops.object.mode_set(mode='POSE')
|
bpy.ops.object.mode_set(mode='POSE')
|
||||||
|
|
||||||
@@ -135,6 +133,7 @@ class AvatarToolKit_OT_DeleteBoneConstraints(Operator):
|
|||||||
constraints_removed += 1
|
constraints_removed += 1
|
||||||
|
|
||||||
bpy.ops.object.mode_set(mode='OBJECT')
|
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))
|
self.report({'INFO'}, t("Tools.clean_constraints_success", count=constraints_removed))
|
||||||
return {'FINISHED'}
|
return {'FINISHED'}
|
||||||
|
|
||||||
@@ -187,6 +186,8 @@ class AvatarToolKit_OT_RemoveZeroWeightBones(Operator):
|
|||||||
# Store initial transforms
|
# Store initial transforms
|
||||||
bpy.ops.object.mode_set(mode='EDIT')
|
bpy.ops.object.mode_set(mode='EDIT')
|
||||||
initial_transforms: Dict[str, Dict[str, Any]] = {}
|
initial_transforms: Dict[str, Dict[str, Any]] = {}
|
||||||
|
data_breaking = store_breaking_settings_armature(armature)
|
||||||
|
|
||||||
for bone in armature.data.edit_bones:
|
for bone in armature.data.edit_bones:
|
||||||
initial_transforms[bone.name] = {
|
initial_transforms[bone.name] = {
|
||||||
'head': bone.head.copy(),
|
'head': bone.head.copy(),
|
||||||
@@ -246,7 +247,7 @@ class AvatarToolKit_OT_RemoveZeroWeightBones(Operator):
|
|||||||
if context.scene.avatar_toolkit.list_only_mode:
|
if context.scene.avatar_toolkit.list_only_mode:
|
||||||
self.populate_bone_list(context, zero_weight_bones)
|
self.populate_bone_list(context, zero_weight_bones)
|
||||||
return {'FINISHED'}
|
return {'FINISHED'}
|
||||||
|
restore_breaking_settings_armature(armature, data_breaking)
|
||||||
self.report({'INFO'}, t("Tools.clean_weights_success", count=removed_count))
|
self.report({'INFO'}, t("Tools.clean_weights_success", count=removed_count))
|
||||||
return {'FINISHED'}
|
return {'FINISHED'}
|
||||||
|
|
||||||
@@ -276,6 +277,7 @@ class AvatarToolKit_OT_RemoveSelectedBones(Operator):
|
|||||||
|
|
||||||
def execute(self, context: Context) -> set[str]:
|
def execute(self, context: Context) -> set[str]:
|
||||||
armature = get_active_armature(context)
|
armature = get_active_armature(context)
|
||||||
|
data_breaking = store_breaking_settings_armature(armature)
|
||||||
toolkit = context.scene.avatar_toolkit
|
toolkit = context.scene.avatar_toolkit
|
||||||
|
|
||||||
selected_bones = [item.name for item in toolkit.zero_weight_bones
|
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')
|
bpy.ops.object.mode_set(mode='OBJECT')
|
||||||
toolkit.zero_weight_bones.clear()
|
toolkit.zero_weight_bones.clear()
|
||||||
|
restore_breaking_settings_armature(armature, data_breaking)
|
||||||
self.report({'INFO'}, t("Tools.bones_removed", count=len(selected_bones)))
|
self.report({'INFO'}, t("Tools.bones_removed", count=len(selected_bones)))
|
||||||
return {'FINISHED'}
|
return {'FINISHED'}
|
||||||
|
|
||||||
@@ -315,7 +317,8 @@ class AvatarToolKit_OT_FlipCurrentKeyFrames(Operator):
|
|||||||
|
|
||||||
def execute(self, context: Context) -> set[str]:
|
def execute(self, context: Context) -> set[str]:
|
||||||
armature = get_active_armature(context)
|
armature = get_active_armature(context)
|
||||||
|
data_breaking = store_breaking_settings_armature(armature)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
armature_data: bpy.types.Armature = armature.data
|
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):
|
#if armature.keyframe_insert(data_path=new_path, index=curve.array_index, frame=time):
|
||||||
continue
|
continue
|
||||||
self.report({'ERROR'}, f"Keyframe insertion for key with data path \"{curve.data_path}\" and frame {time} failed!")
|
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'}
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
|
||||||
@@ -397,4 +401,5 @@ class AvatarToolKit_OT_FlipCurrentKeyFrames(Operator):
|
|||||||
|
|
||||||
# restore selection
|
# restore selection
|
||||||
armature_data.bones.foreach_set("select", selected)
|
armature_data.bones.foreach_set("select", selected)
|
||||||
|
restore_breaking_settings_armature(armature, data_breaking)
|
||||||
return {'FINISHED'}
|
return {'FINISHED'}
|
||||||
|
|||||||
@@ -4,8 +4,9 @@ from typing import Set, List
|
|||||||
from bpy.types import Operator, Context, Armature, EditBone
|
from bpy.types import Operator, Context, Armature, EditBone
|
||||||
from ...core.translations import t
|
from ...core.translations import t
|
||||||
from ...core.logging_setup import logger
|
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
|
from ...core.armature_validation import validate_armature
|
||||||
|
import traceback
|
||||||
|
|
||||||
class AvatarToolkit_OT_ConnectBones(Operator):
|
class AvatarToolkit_OT_ConnectBones(Operator):
|
||||||
"""Connect disconnected bones in chain"""
|
"""Connect disconnected bones in chain"""
|
||||||
@@ -23,8 +24,12 @@ class AvatarToolkit_OT_ConnectBones(Operator):
|
|||||||
return valid
|
return valid
|
||||||
|
|
||||||
def execute(self, context: Context) -> Set[str]:
|
def execute(self, context: Context) -> Set[str]:
|
||||||
|
armature = get_active_armature(context)
|
||||||
|
data_breaking = store_breaking_settings_armature(armature)
|
||||||
try:
|
try:
|
||||||
armature = get_active_armature(context)
|
|
||||||
|
|
||||||
|
|
||||||
logger.info("Starting bone connection operation")
|
logger.info("Starting bone connection operation")
|
||||||
|
|
||||||
bpy.ops.object.mode_set(mode='EDIT')
|
bpy.ops.object.mode_set(mode='EDIT')
|
||||||
@@ -47,12 +52,14 @@ class AvatarToolkit_OT_ConnectBones(Operator):
|
|||||||
bones_connected += 1
|
bones_connected += 1
|
||||||
|
|
||||||
bpy.ops.object.mode_set(mode='OBJECT')
|
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))
|
self.report({'INFO'}, t("Tools.connect_bones_success", count=bones_connected))
|
||||||
return {'FINISHED'}
|
return {'FINISHED'}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception:
|
||||||
logger.error(f"Failed to connect bones: {str(e)}")
|
logger.error(f"Failed to connect bones: {traceback.format_exc()}")
|
||||||
self.report({'ERROR'}, str(e))
|
self.report({'ERROR'}, traceback.format_exc())
|
||||||
|
restore_breaking_settings_armature(armature, data_breaking)
|
||||||
return {'CANCELLED'}
|
return {'CANCELLED'}
|
||||||
|
|
||||||
class AvatarToolkit_OT_MergeToActive(Operator):
|
class AvatarToolkit_OT_MergeToActive(Operator):
|
||||||
@@ -67,11 +74,15 @@ class AvatarToolkit_OT_MergeToActive(Operator):
|
|||||||
armature = get_active_armature(context)
|
armature = get_active_armature(context)
|
||||||
if not armature:
|
if not armature:
|
||||||
return False
|
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]:
|
def execute(self, context: Context) -> Set[str]:
|
||||||
|
armature = get_active_armature(context)
|
||||||
|
data_breaking = store_breaking_settings_armature(armature)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
armature = get_active_armature(context)
|
bpy.ops.object.mode_set(mode='EDIT')
|
||||||
|
|
||||||
active_bone = context.active_bone
|
active_bone = context.active_bone
|
||||||
selected_bones = [b for b in context.selected_editable_bones if b != 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)
|
armature.data.edit_bones.remove(bone)
|
||||||
|
|
||||||
self.report({'INFO'}, t("Tools.merge_to_active_success", count=len(selected_bones)))
|
self.report({'INFO'}, t("Tools.merge_to_active_success", count=len(selected_bones)))
|
||||||
|
restore_breaking_settings_armature(armature, data_breaking)
|
||||||
return {'FINISHED'}
|
return {'FINISHED'}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception:
|
||||||
logger.error(f"Failed to merge bones: {str(e)}")
|
logger.error(f"Failed to merge bones: {traceback.format_exc()}")
|
||||||
self.report({'ERROR'}, str(e))
|
self.report({'ERROR'}, traceback.format_exc())
|
||||||
|
restore_breaking_settings_armature(armature, data_breaking)
|
||||||
return {'CANCELLED'}
|
return {'CANCELLED'}
|
||||||
|
|
||||||
class AvatarToolkit_OT_MergeToParent(Operator):
|
class AvatarToolkit_OT_MergeToParent(Operator):
|
||||||
@@ -121,11 +134,13 @@ class AvatarToolkit_OT_MergeToParent(Operator):
|
|||||||
armature = get_active_armature(context)
|
armature = get_active_armature(context)
|
||||||
if not armature:
|
if not armature:
|
||||||
return False
|
return False
|
||||||
return context.mode == 'EDIT_ARMATURE'
|
return (context.mode == 'EDIT_ARMATURE' or context.mode == 'POSE')
|
||||||
|
|
||||||
def execute(self, context: Context) -> Set[str]:
|
def execute(self, context: Context) -> Set[str]:
|
||||||
|
armature = get_active_armature(context)
|
||||||
|
data_breaking = store_breaking_settings_armature(armature)
|
||||||
try:
|
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]
|
selected_bones = [b for b in context.selected_editable_bones if b.parent]
|
||||||
|
|
||||||
if not selected_bones:
|
if not selected_bones:
|
||||||
@@ -153,10 +168,12 @@ class AvatarToolkit_OT_MergeToParent(Operator):
|
|||||||
armature.data.edit_bones.remove(bone)
|
armature.data.edit_bones.remove(bone)
|
||||||
merged_count += 1
|
merged_count += 1
|
||||||
|
|
||||||
|
restore_breaking_settings_armature(armature, data_breaking)
|
||||||
self.report({'INFO'}, t("Tools.merge_to_parent_success", count=merged_count))
|
self.report({'INFO'}, t("Tools.merge_to_parent_success", count=merged_count))
|
||||||
return {'FINISHED'}
|
return {'FINISHED'}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception:
|
||||||
logger.error(f"Failed to merge bones: {str(e)}")
|
logger.error(f"Failed to merge bones: {traceback.format_exc()}")
|
||||||
self.report({'ERROR'}, str(e))
|
self.report({'ERROR'}, traceback.format_exc())
|
||||||
|
restore_breaking_settings_armature(armature, data_breaking)
|
||||||
return {'CANCELLED'}
|
return {'CANCELLED'}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from bpy.types import Operator, Context
|
|||||||
from ...core.translations import t
|
from ...core.translations import t
|
||||||
from ...core.common import get_active_armature
|
from ...core.common import get_active_armature
|
||||||
from ...core.armature_validation import validate_armature
|
from ...core.armature_validation import validate_armature
|
||||||
|
import traceback
|
||||||
|
|
||||||
class AvatarToolKit_OT_SeparateByMaterials(Operator):
|
class AvatarToolKit_OT_SeparateByMaterials(Operator):
|
||||||
"""Operator to separate mesh by materials"""
|
"""Operator to separate mesh by materials"""
|
||||||
@@ -32,8 +33,8 @@ class AvatarToolKit_OT_SeparateByMaterials(Operator):
|
|||||||
bpy.ops.object.mode_set(mode='OBJECT')
|
bpy.ops.object.mode_set(mode='OBJECT')
|
||||||
self.report({'INFO'}, t("Tools.separate_materials_success"))
|
self.report({'INFO'}, t("Tools.separate_materials_success"))
|
||||||
return {'FINISHED'}
|
return {'FINISHED'}
|
||||||
except Exception as e:
|
except Exception:
|
||||||
self.report({'ERROR'}, str(e))
|
self.report({'ERROR'}, traceback.format_exc())
|
||||||
return {'CANCELLED'}
|
return {'CANCELLED'}
|
||||||
|
|
||||||
class AvatarToolKit_OT_SeparateByLooseParts(Operator):
|
class AvatarToolKit_OT_SeparateByLooseParts(Operator):
|
||||||
@@ -64,6 +65,6 @@ class AvatarToolKit_OT_SeparateByLooseParts(Operator):
|
|||||||
bpy.ops.object.mode_set(mode='OBJECT')
|
bpy.ops.object.mode_set(mode='OBJECT')
|
||||||
self.report({'INFO'}, t("Tools.separate_loose_success"))
|
self.report({'INFO'}, t("Tools.separate_loose_success"))
|
||||||
return {'FINISHED'}
|
return {'FINISHED'}
|
||||||
except Exception as e:
|
except Exception:
|
||||||
self.report({'ERROR'}, str(e))
|
self.report({'ERROR'}, traceback.format_exc())
|
||||||
return {'CANCELLED'}
|
return {'CANCELLED'}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from ...core.logging_setup import logger
|
|||||||
from ...core.translations import t
|
from ...core.translations import t
|
||||||
from ...core.dictionaries import rigify_unity_names, rigify_basic_unity_names, rigify_unnecessary_bones
|
from ...core.dictionaries import rigify_unity_names, rigify_basic_unity_names, rigify_unnecessary_bones
|
||||||
from ...core.armature_validation import validate_armature
|
from ...core.armature_validation import validate_armature
|
||||||
|
import traceback
|
||||||
|
|
||||||
class AvatarToolkit_OT_ConvertRigifyToUnity(Operator):
|
class AvatarToolkit_OT_ConvertRigifyToUnity(Operator):
|
||||||
"""Convert Rigify armature to Unity-compatible format"""
|
"""Convert Rigify armature to Unity-compatible format"""
|
||||||
@@ -56,9 +57,9 @@ class AvatarToolkit_OT_ConvertRigifyToUnity(Operator):
|
|||||||
self.report({'INFO'}, t("Tools.rigify_converted"))
|
self.report({'INFO'}, t("Tools.rigify_converted"))
|
||||||
return {'FINISHED'}
|
return {'FINISHED'}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception:
|
||||||
logger.error(f"Failed to convert Rigify: {str(e)}", exc_info=True)
|
logger.error(f"Failed to convert Rigify: {traceback.format_exc()}", exc_info=True)
|
||||||
self.report({'ERROR'}, str(e))
|
self.report({'ERROR'}, traceback.format_exc())
|
||||||
return {'CANCELLED'}
|
return {'CANCELLED'}
|
||||||
|
|
||||||
def cleanup_extra_bones(self, armature: Object) -> None:
|
def cleanup_extra_bones(self, armature: Object) -> None:
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import traceback
|
||||||
import bpy
|
import bpy
|
||||||
import math
|
import math
|
||||||
from typing import Dict, List, Set, Tuple, Optional, Any, Union
|
from typing import Dict, List, Set, Tuple, Optional, Any, Union
|
||||||
@@ -25,7 +26,7 @@ class AvatarToolkit_OT_StandardizeArmature(Operator):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context: Context) -> bool:
|
def poll(cls, context: Context) -> bool:
|
||||||
armature: Optional[Object] = get_active_armature(context)
|
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]:
|
def invoke(self, context: Context, event: Any) -> Set[str]:
|
||||||
logger.debug("Invoking standardize armature dialog")
|
logger.debug("Invoking standardize armature dialog")
|
||||||
@@ -99,20 +100,24 @@ class AvatarToolkit_OT_StandardizeArmature(Operator):
|
|||||||
|
|
||||||
if original_mode == 'EDIT_ARMATURE':
|
if original_mode == 'EDIT_ARMATURE':
|
||||||
bpy.ops.object.mode_set(mode='EDIT')
|
bpy.ops.object.mode_set(mode='EDIT')
|
||||||
|
if original_mode == 'POSE':
|
||||||
|
bpy.ops.object.mode_set(mode='POSE')
|
||||||
|
|
||||||
return {'FINISHED'}
|
return {'FINISHED'}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception:
|
||||||
logger.error(f"Failed to standardize armature: {str(e)}")
|
logger.error(f"Failed to standardize armature: {traceback.format_exc()}")
|
||||||
self.report({'ERROR'}, str(e))
|
self.report({'ERROR'}, traceback.format_exc())
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if original_mode == 'EDIT_ARMATURE':
|
if original_mode == 'EDIT_ARMATURE':
|
||||||
bpy.ops.object.mode_set(mode='EDIT')
|
bpy.ops.object.mode_set(mode='EDIT')
|
||||||
|
if original_mode == 'POSE':
|
||||||
|
bpy.ops.object.mode_set(mode='POSE')
|
||||||
else:
|
else:
|
||||||
bpy.ops.object.mode_set(mode='OBJECT')
|
bpy.ops.object.mode_set(mode='OBJECT')
|
||||||
except Exception as restore_error:
|
except Exception:
|
||||||
logger.error(f"Failed to restore original mode: {str(restore_error)}")
|
logger.error(f"Failed to restore original mode: {traceback.format_exc()}")
|
||||||
|
|
||||||
return {'CANCELLED'}
|
return {'CANCELLED'}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import numpy as np
|
|||||||
import math
|
import math
|
||||||
from ...core.translations import t
|
from ...core.translations import t
|
||||||
from ...core.logging_setup import logger
|
from ...core.logging_setup import logger
|
||||||
|
import traceback
|
||||||
|
|
||||||
class GenerateLoopTreeResult(TypedDict):
|
class GenerateLoopTreeResult(TypedDict):
|
||||||
tree: Dict[str, Set[str]]
|
tree: Dict[str, Set[str]]
|
||||||
@@ -247,8 +248,8 @@ class AvatarToolkit_OT_AlignUVEdgesToTarget(Operator):
|
|||||||
|
|
||||||
logger.info(f"Finished mesh {source} for UV's")
|
logger.info(f"Finished mesh {source} for UV's")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception:
|
||||||
logger.error(f"Error processing source {source}: {str(e)}")
|
logger.error(f"Error processing source {source}: {traceback.format_exc()}")
|
||||||
return {'CANCELLED'}
|
return {'CANCELLED'}
|
||||||
|
|
||||||
bpy.ops.object.mode_set(mode=prev_mode)
|
bpy.ops.object.mode_set(mode=prev_mode)
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ from ..core.common import (
|
|||||||
get_all_meshes,
|
get_all_meshes,
|
||||||
validate_mesh_for_pose
|
validate_mesh_for_pose
|
||||||
)
|
)
|
||||||
|
import traceback
|
||||||
|
|
||||||
class VisemeCache:
|
class VisemeCache:
|
||||||
"""Manages caching of generated viseme shape data for performance optimization"""
|
"""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.create_visemes(context, mesh)
|
||||||
self.report({'INFO'}, t("Visemes.success"))
|
self.report({'INFO'}, t("Visemes.success"))
|
||||||
return {'FINISHED'}
|
return {'FINISHED'}
|
||||||
except Exception as e:
|
except Exception:
|
||||||
logger.error(f"Error creating visemes: {str(e)}")
|
logger.error(f"Error creating visemes: {traceback.format_exc()}")
|
||||||
self.report({'ERROR'}, str(e))
|
self.report({'ERROR'}, traceback.format_exc())
|
||||||
return {'CANCELLED'}
|
return {'CANCELLED'}
|
||||||
|
|
||||||
def create_visemes(self, context: Context, mesh: Object) -> None:
|
def create_visemes(self, context: Context, mesh: Object) -> None:
|
||||||
|
|||||||
@@ -228,6 +228,11 @@
|
|||||||
"Tools.explode_mesh.distance_desc": "Scale factor for distance between exploded items on model.",
|
"Tools.explode_mesh.distance_desc": "Scale factor for distance between exploded items on model.",
|
||||||
"Tools.explode_mesh.split_on_seams_desc":"Split model on UV seams to separate islands from each other.",
|
"Tools.explode_mesh.split_on_seams_desc":"Split model on UV seams to separate islands from each other.",
|
||||||
"Tools.explode_mesh.split_on_seams":"Split on Seams",
|
"Tools.explode_mesh.split_on_seams":"Split on Seams",
|
||||||
|
"Tools.shapekey_to_basis.label":"Apply Selected Shapekey to Basis",
|
||||||
|
"Tools.shapekey_to_basis.desc":"Applies the selected shape key to the new Basis at it's current strength and creates a reverted shape key from the selected one.",
|
||||||
|
"ShapeKeyApplier.error.recursiveRelativeToLoop":"Shapekey \"{name}\" is recursively relative to itself, so cannot be applied to the Basis",
|
||||||
|
"ShapeKeyApplier.successRemoved":"Successfully removed shapekey \"{name}\" from the Basis.",
|
||||||
|
"ShapeKeyApplier.successSet":"Successfully applied shapekey \"{name}\" to the Basis.",
|
||||||
"Tools.apply_modifier_on_shapekey_obj":"Apply Modifier on Shapekey Object",
|
"Tools.apply_modifier_on_shapekey_obj":"Apply Modifier on Shapekey Object",
|
||||||
"Tools.apply_modifier_on_shapekey_obj_desc":"Applies a modifier on an object regardless of it having shapekeys.",
|
"Tools.apply_modifier_on_shapekey_obj_desc":"Applies a modifier on an object regardless of it having shapekeys.",
|
||||||
"Tools.merge_title": "Merge Tools",
|
"Tools.merge_title": "Merge Tools",
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from ..core.common import SceneMatClass, MaterialListBool, get_active_armature
|
|||||||
from ..functions.atlas_materials import AvatarToolKit_OT_AtlasMaterials
|
from ..functions.atlas_materials import AvatarToolKit_OT_AtlasMaterials
|
||||||
from ..core.translations import t
|
from ..core.translations import t
|
||||||
from ..core.logging_setup import logger
|
from ..core.logging_setup import logger
|
||||||
|
import traceback
|
||||||
|
|
||||||
class AvatarToolKit_OT_SelectAllMaterials(Operator):
|
class AvatarToolKit_OT_SelectAllMaterials(Operator):
|
||||||
bl_idname = 'avatar_toolkit.select_all_materials'
|
bl_idname = 'avatar_toolkit.select_all_materials'
|
||||||
@@ -80,8 +81,8 @@ class AvatarToolKit_OT_ExpandSectionMaterials(Operator):
|
|||||||
logger.debug("Hiding material list")
|
logger.debug("Hiding material list")
|
||||||
|
|
||||||
return {'FINISHED'}
|
return {'FINISHED'}
|
||||||
except Exception as e:
|
except Exception:
|
||||||
logger.error(f"Error loading materials: {str(e)}", exc_info=True)
|
logger.error(f"Error loading materials: {traceback.format_exc()}", exc_info=True)
|
||||||
self.report({'ERROR'}, t("TextureAtlas.load_error"))
|
self.report({'ERROR'}, t("TextureAtlas.load_error"))
|
||||||
return {'CANCELLED'}
|
return {'CANCELLED'}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user