diff --git a/core/common.py b/core/common.py index 6e79d9c..30cd235 100644 --- a/core/common.py +++ b/core/common.py @@ -74,20 +74,63 @@ def clean_material_names(mesh: Mesh) -> None: def fix_uv_coordinates(context: Context) -> None: obj = context.object - # Check if the object is in Edit Mode - if obj.mode != 'EDIT': - bpy.ops.object.mode_set(mode='EDIT') + # Store current mode and selection + current_mode = context.mode + current_active = context.view_layer.objects.active + current_selected = context.selected_objects.copy() + + # Ensure we're in object mode and select the object + bpy.ops.object.mode_set(mode='OBJECT') + obj.select_set(True) + context.view_layer.objects.active = obj # Check if the object has any mesh data if obj.type == 'MESH' and obj.data: - bpy.context.view_layer.objects.active = obj + + # Switch to Edit Mode + bpy.ops.object.mode_set(mode='EDIT') + + # Select all UVs bpy.ops.mesh.select_all(action='SELECT') - bpy.ops.uv.average_islands_scale() + + # Try to find UV Editor area, fall back to 3D View if not found + area = next((area for area in context.screen.areas if area.type == 'UV_EDITOR'), None) + if not area: + area = next((area for area in context.screen.areas if area.type == 'VIEW_3D'), None) + + # Get the region and space data + region = next((region for region in area.regions if region.type == 'WINDOW'), None) + space_data = area.spaces.active + + # Create a context override + override = { + 'area': area, + 'region': region, + 'space_data': space_data, + 'edit_object': obj, + 'active_object': obj, + 'selected_objects': [obj], + 'mode': 'EDIT_MESH', + } + + try: + # Ensure UVs are selected + bpy.ops.uv.select_all(override, action='SELECT') + # Average UV island scales + bpy.ops.uv.average_islands_scale(override) + except Exception as e: + print(f"UV Fix - Error during UV scaling: {str(e)}") # Switch back to Object Mode bpy.ops.object.mode_set(mode='OBJECT') + print("UV Fix - Switched back to Object Mode") + + # Restore previous selection and active object + for sel_obj in current_selected: + sel_obj.select_set(True) + context.view_layer.objects.active = current_active else: - print("Object is not a valid mesh with UV data") + print("UV Fix - Object is not a valid mesh with UV data") def has_shapekeys(mesh_obj: Object) -> bool: return mesh_obj.data.shape_keys is not None @@ -424,23 +467,33 @@ def finish_progress(context): context.area.header_text_set(None) def transfer_vertex_weights(context: Context, obj: bpy.types.Object, source_group: str, target_group: str, delete_source_group: bool = True) -> bool: - - modifier: bpy.types.VertexWeightMixModifier = obj.modifiers.new(name="merge_weights",type="VERTEX_WEIGHT_MIX") - - modifier.mix_set = 'B' + # Create and configure the Vertex Weight Mix modifier + modifier = obj.modifiers.new(name="merge_weights", type="VERTEX_WEIGHT_MIX") + modifier.show_viewport = True + modifier.show_render = True + modifier.mix_set = 'B' # Replace weights in A with weights from B modifier.vertex_group_a = target_group modifier.vertex_group_b = source_group modifier.mask_constant = 1.0 + # Ensure we're in Object Mode bpy.ops.object.mode_set(mode='OBJECT') - prev_obj: bpy.types.Object = context.view_layer.objects.active + + # Deselect all objects and select only our target object + bpy.ops.object.select_all(action='DESELECT') + obj.select_set(True) context.view_layer.objects.active = obj + + # Move modifier to the top of the stack if necessary + if len(obj.modifiers) > 1: + obj.modifiers.move(obj.modifiers.find(modifier.name), 0) + + # Apply modifier bpy.ops.object.modifier_apply(modifier=modifier.name) - if delete_source_group: - obj.vertex_groups.remove(obj.vertex_groups.get(source_group)) - bpy.ops.object.mode_set(mode='EDIT') - bpy.ops.object.mode_set(mode='OBJECT') - context.view_layer.objects.active = prev_obj + + # Clean up + if delete_source_group and source_group in obj.vertex_groups: + obj.vertex_groups.remove(obj.vertex_groups[source_group]) return True diff --git a/functions/armature_modifying.py b/functions/armature_modifying.py index ddf467a..efc109f 100644 --- a/functions/armature_modifying.py +++ b/functions/armature_modifying.py @@ -117,7 +117,6 @@ class AvatarToolkit_OT_ApplyPoseAsRest(Operator): return {'CANCELLED'} return {'FINISHED'} - @register_wrap class AvatarToolkit_OT_RemoveZeroWeightBones(Operator): bl_idname = "avatar_toolkit.remove_zero_weight_bones" @@ -144,10 +143,22 @@ class AvatarToolkit_OT_RemoveZeroWeightBones(Operator): weighted_bones: list[str] = [] - bpy.ops.object.mode_set(mode='OBJECT') bpy.ops.object.select_all(action='DESELECT') + # Store initial transforms + initial_transforms = {} + bpy.ops.object.mode_set(mode='EDIT') + for bone in armature.data.edit_bones: + initial_transforms[bone.name] = { + 'head': bone.head.copy(), + 'tail': bone.tail.copy(), + 'roll': bone.roll, + 'matrix': bone.matrix.copy(), + 'parent': bone.parent.name if bone.parent else None + } + + # Get weighted bones armature.select_set(True) context.view_layer.objects.active = armature @@ -157,26 +168,56 @@ class AvatarToolkit_OT_RemoveZeroWeightBones(Operator): for vertex in mesh_data.vertices: for group in vertex.groups: if group.weight > self.threshold: - weighted_bones.append(mesh.vertex_groups[group.group].name) #add bone name to list of bones that are greater than the weight threshold + weighted_bones.append(mesh.vertex_groups[group.group].name) bpy.ops.object.mode_set(mode='EDIT') amature_data: Armature = armature.data unweighted_bones: list[str] = [] - #doing 2 loops to prevent modification of array during iteration + # Identify unweighted bones for bone in amature_data.edit_bones: if bone.name not in weighted_bones: - unweighted_bones.append(bone.name) #add bones that arent in the list of bones that have weight into the list of bones that don't + unweighted_bones.append(bone.name) + # Process bone removal while preserving positions for bone_name in unweighted_bones: - for edit_bone in amature_data.edit_bones[bone_name].children: - edit_bone.use_connect = False #to fix randomly moving bones - edit_bone.parent = amature_data.edit_bones[bone_name].parent #to fix unparented bones. - amature_data.edit_bones.remove(amature_data.edit_bones[bone_name]) #delete list of unweighted bones from the armature + bone = amature_data.edit_bones[bone_name] + + # Store children data + children = bone.children + children_data = {} + for child in children: + children_data[child.name] = initial_transforms[child.name] + + # Reparent children + for child in children: + child.use_connect = False + if bone.parent: + child.parent = bone.parent + + # Remove bone + amature_data.edit_bones.remove(bone) + + # Restore children positions + for child_name, data in children_data.items(): + if child_name in amature_data.edit_bones: + child = amature_data.edit_bones[child_name] + child.head = data['head'] + child.tail = data['tail'] + child.roll = data['roll'] + child.matrix = data['matrix'] + # Final position verification + for bone_name, transform in initial_transforms.items(): + if bone_name in amature_data.edit_bones: + bone = amature_data.edit_bones[bone_name] + bone.matrix = transform['matrix'] + + bpy.ops.object.mode_set(mode='OBJECT') self.report({'INFO'}, t("Tools.remove_zero_weight_bones.success")) return {'FINISHED'} + @register_wrap class AvatarToolkit_OT_MergeBonesToActive(Operator): bl_idname = "avatar_toolkit.merge_bones_to_active" @@ -233,44 +274,74 @@ class AvatarToolkit_OT_MergeBonesToParents(Operator): bl_description = t("Tools.merge_bones_to_parents.desc") bl_options = {'REGISTER', 'UNDO'} - delete_old: bpy.props.BoolProperty(name=t("Tools.merge_bones_to_parents.delete_old.label"), description=t("Tools.merge_bones_to_parents.delete_old.desc"), default=False) + delete_old: bpy.props.BoolProperty( + name=t("Tools.merge_bones_to_parents.delete_old.label"), + description=t("Tools.merge_bones_to_parents.delete_old.desc"), + default=False + ) @classmethod def poll(cls, context: Context) -> bool: - if common.get_selected_armature(context) is not None: - if common.get_selected_armature(context) == context.view_layer.objects.active: - if context.mode == "POSE": - return len(context.selected_pose_bones) > 0 - elif context.mode == "EDIT_ARMATURE": - return len(context.selected_bones) > 0 + armature = common.get_selected_armature(context) + if armature and armature == context.view_layer.objects.active: + if context.mode == "POSE": + return len(context.selected_pose_bones) > 0 + elif context.mode == "EDIT_ARMATURE": + return len(context.selected_editable_bones) > 0 return False - def execute(cls, context: Context) -> set[str]: + def execute(self, context: Context) -> set[str]: + prev_mode = context.mode - prev_mode: str = "EDIT" - if context.mode == "POSE": - prev_mode = "POSE" - #get active bone and a list of all other selected bones + # Map 'EDIT_ARMATURE' to 'EDIT' for bpy.ops.object.mode_set + if prev_mode == 'EDIT_ARMATURE': + prev_mode = 'EDIT' + + # Switch to Edit Mode bpy.ops.object.mode_set(mode='EDIT') armature_data: Armature = context.view_layer.objects.active.data + # Get selected bones in Edit Mode + selected_bones = context.selected_editable_bones + selected_bone_names = [bone.name for bone in selected_bones] + + if not selected_bone_names: + self.report({'ERROR'}, t("No bones selected")) + return {'CANCELLED'} + for obj in common.get_all_meshes(context): - for bone in [i.name for i in context.selected_bones]: - if armature_data.edit_bones[bone].parent != None: - bone_name: str = armature_data.edit_bones[bone].name - common.transfer_vertex_weights(context=context,obj=obj,source_group=bone_name,target_group=armature_data.edit_bones[bone].parent.name) + for bone_name in selected_bone_names: + bone = armature_data.edit_bones.get(bone_name) + if bone and bone.parent: + # Transfer weights from bone to its parent + common.transfer_vertex_weights( + context=context, + obj=obj, + source_group=bone_name, + target_group=bone.parent.name + ) + # Ensure we're in Edit Mode after transfer bpy.ops.object.mode_set(mode='EDIT') - - for bone in [i.name for i in context.selected_bones]: - if cls.delete_old: - for bone_child in armature_data.edit_bones[bone].children: - bone_child.parent = armature_data.edit_bones[bone].parent - armature_data.edit_bones.remove(armature_data.edit_bones[bone]) - + else: + self.report({'WARNING'}, f"Bone '{bone_name}' has no parent or not found; skipping") + + # Optionally delete old bones + if self.delete_old: + for bone_name in selected_bone_names: + bone = armature_data.edit_bones.get(bone_name) + if bone: + # Reassign children to the parent of the bone being deleted + for child in bone.children: + child.parent = bone.parent + # Remove the bone + armature_data.edit_bones.remove(bone) + else: + self.report({'WARNING'}, f"Bone '{bone_name}' not found in armature; cannot delete") + + # Return to previous mode bpy.ops.object.mode_set(mode=prev_mode) return {'FINISHED'} - @register_wrap class AvatarToolkit_OT_MergeArmatures(Operator): bl_idname = "avatar_toolkit.merge_armatures" diff --git a/functions/mesh_tools.py b/functions/mesh_tools.py index 426dc54..e814932 100644 --- a/functions/mesh_tools.py +++ b/functions/mesh_tools.py @@ -104,10 +104,13 @@ class AvatarToolKit_OT_JoinAllMeshes(Operator): raise ValueError(t("Optimization.no_armature_selected")) armature = get_selected_armature(context) + bpy.ops.object.mode_set(mode='OBJECT') + bpy.ops.object.select_all(action='DESELECT') meshes: List[Object] = get_all_meshes(context) + if not meshes: raise ValueError(t("Optimization.no_meshes_found")) @@ -133,6 +136,7 @@ class AvatarToolKit_OT_JoinAllMeshes(Operator): raise RuntimeError(f"{t('Optimization.transform_apply_failed')}: {str(e)}") update_progress(self, context, t("Optimization.fixing_uv_coordinates")) + bpy.ops.object.mode_set(mode='OBJECT') fix_uv_coordinates(context) update_progress(self, context, t("Optimization.finalizing")) @@ -145,6 +149,7 @@ class AvatarToolKit_OT_JoinAllMeshes(Operator): context.view_layer.objects.active = armature finish_progress(context) + @register_wrap class AvatarToolKit_OT_JoinSelectedMeshes(Operator): bl_idname = "avatar_toolkit.join_selected_meshes" diff --git a/functions/mmd_functions.py b/functions/mmd_functions.py index c1d66f6..daaca13 100644 --- a/functions/mmd_functions.py +++ b/functions/mmd_functions.py @@ -123,19 +123,23 @@ class AvatarToolKit_OT_OptimizeArmature(Operator): init_progress(context, 9) - update_progress(self, context, t("MMDOptions.fixing_bone_rolls")) - bpy.ops.object.mode_set(mode='EDIT') - for bone in armature.data.edit_bones: - bone.roll = 0 - - update_progress(self, context, t("MMDOptions.aligning_bones")) - for bone in armature.data.edit_bones: - if bone.parent: - bone.head = bone.parent.tail - - update_progress(self, context, t("MMDOptions.connecting_bones")) + # Ensure proper object selection and mode bpy.ops.object.mode_set(mode='OBJECT') - bpy.ops.avatar_toolkit.connect_bones('EXEC_DEFAULT') + bpy.ops.object.select_all(action='DESELECT') + armature.select_set(True) + context.view_layer.objects.active = armature + + # Store initial transforms + bpy.ops.object.mode_set(mode='EDIT') + initial_transforms = {} + for bone in armature.data.edit_bones: + initial_transforms[bone.name] = { + 'head': bone.head.copy(), + 'tail': bone.tail.copy(), + 'roll': bone.roll, + 'matrix': bone.matrix.copy(), + 'parent': bone.parent.name if bone.parent else None + } update_progress(self, context, t("MMDOptions.deleting_bone_constraints")) bpy.ops.avatar_toolkit.delete_bone_constraints('EXEC_DEFAULT') @@ -160,7 +164,24 @@ class AvatarToolKit_OT_OptimizeArmature(Operator): update_progress(self, context, t("MMDOptions.renaming_bones")) self.rename_bones(armature) + # Restore original bone transforms + bpy.ops.object.mode_set(mode='EDIT') + for bone_name, transform in initial_transforms.items(): + if bone_name in armature.data.edit_bones: + bone = armature.data.edit_bones[bone_name] + bone.head = transform['head'] + bone.tail = transform['tail'] + bone.roll = transform['roll'] + bone.matrix = transform['matrix'] + update_progress(self, context, t("MMDOptions.armature_optimization_complete")) + + # Ensure we end in object mode with proper selection + bpy.ops.object.mode_set(mode='OBJECT') + bpy.ops.object.select_all(action='DESELECT') + armature.select_set(True) + context.view_layer.objects.active = armature + finish_progress(context) return {'FINISHED'}