Files
Avatar-Toolkit/functions/optimization/remove_doubles.py
T
989onan 88e88b94a3 hotfix
2025-04-03 20:14:17 -04:00

191 lines
8.1 KiB
Python

import traceback
import bpy
import numpy as np
from typing import List, TypedDict, Any, Literal, TypeAlias, cast
from bpy.types import Operator, Context, Object, Event
from ...core.logging_setup import logger
from ...core.translations import t
from ...core.common import (
get_active_armature,
get_all_meshes,
)
from ...core.armature_validation import validate_armature
import bmesh
import mathutils
# Constants
MERGE_ITERATION_COUNT = 20
MERGE_DISTANCE_DEFAULT = 0.0001
# Type definitions
ModalReturnType: TypeAlias = Literal['RUNNING_MODAL', 'FINISHED', 'CANCELLED']
class MeshEntry(TypedDict):
mesh: Object
shapekeys: list[bpy.types.Object]
def create_duplicate_for_merge(context: Context, mesh: Object, shapekey_name: str = "") -> Object:
"""Creates a duplicate mesh object for merge testing"""
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.select_all(action='DESELECT')
mesh.select_set(True)
context.view_layer.objects.active = mesh
bpy.ops.object.duplicate()
duplicate = context.view_layer.objects.active
if(shapekey_name != ""):
for shape in duplicate.data.shape_keys.key_blocks:
shape.value = 0
duplicate.active_shape_key_index = mesh.data.shape_keys.key_blocks.find(shapekey_name)
duplicate.active_shape_key.value = 1
bpy.ops.object.shape_key_remove(all=True,apply_mix=True)
duplicate.name = f"{shapekey_name}_object_is_{mesh.name}"
else:
duplicate.name = f"object_is_{mesh.name}"
return duplicate
def select_obj(context: Context, obj: Object, target_mode='OBJECT'):
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.select_all(action='DESELECT')
obj.select_set(True)
context.view_layer.objects.active = obj
bpy.ops.object.mode_set(mode=target_mode)
class AvatarToolkit_OT_RemoveDoubles(Operator):
bl_idname = "avatar_toolkit.remove_doubles"
bl_label = t("Optimization.remove_doubles")
bl_description = t("Optimization.remove_doubles_desc")
bl_options = {'REGISTER', 'UNDO'}
objects_to_do: list[MeshEntry] = []
merge_distance: bpy.props.FloatProperty(name=t("Optimization.merge_distance"), description=t("Optimization.merge_distance_desc"), default=.001)
@classmethod
def poll(cls, context: Context) -> bool:
"""Check if the operator can be executed"""
armature = get_active_armature(context)
if not armature:
return False
valid, _, _ = validate_armature(armature)
return valid
def draw(self, context: Context) -> None:
"""Draw the operator's UI"""
layout = self.layout
layout.prop(self, "merge_distance")
def invoke(self, context: Context, event: Event) -> set[str]:
"""Initialize the operator"""
logger.info("Starting modal execution of merge doubles safely")
return context.window_manager.invoke_props_dialog(self)
def setup_mesh_entry(self, context: Context, mesh: Object) -> MeshEntry:
"""Set up mesh entry data structure"""
#create shapekey objects to merge doubles on.
shapes: list[bpy.types.Object] = []
if(mesh.data.shape_keys):
for shape in mesh.data.shape_keys.key_blocks:
shapes.append(create_duplicate_for_merge(context,mesh,shape.name))
else:
shapes.append(create_duplicate_for_merge(context,mesh))
mesh_entry: MeshEntry = {
"mesh": mesh,
"shapekeys": shapes
}
return mesh_entry
def execute(self, context: Context) -> set[str]:
"""Execute the remove doubles operator"""
try:
armature = get_active_armature(context)
if not armature:
self.report({'WARNING'}, t("Optimization.no_armature"))
return {'CANCELLED'}
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.select_all(action='DESELECT')
objects = get_all_meshes(context)
self.objects_to_do = []
for mesh in objects:
if mesh.data.name not in [obj["mesh"].data.name for obj in self.objects_to_do]:
logger.debug(f"Setting up data for object {mesh.name}")
mesh_entry = self.setup_mesh_entry(context, mesh)
self.objects_to_do.append(mesh_entry)
context.window_manager.modal_handler_add(self)
return {'RUNNING_MODAL'}
except Exception as e:
logger.error(f"Error in execute: {str(e)}")
return {'CANCELLED'}
def modal(self, context: Context, event: Event) -> set[ModalReturnType]:
"""Modal operator execution"""
try:
if not self.objects_to_do or len(self.objects_to_do) <= 0:
self.report({'INFO'}, t("Optimization.remove_doubles_completed"))
logger.info("Finishing modal execution of merge doubles safely")
return {'FINISHED'}
mesh: MeshEntry = self.objects_to_do.pop(0)
merge_distance: float = self.merge_distance
#find which vertices merge on all shapekeys using bmesh, a fast way of doing it - @989onan
#final_merged_vertex_group = [i for i in range(0,len(mesh['mesh'].data.vertices))]
final_merged_vertex_group: dict[set[int],list[int]] = []
for shape in mesh["shapekeys"]:
select_obj(context, shape, target_mode='EDIT')
bmesh_mesh: bmesh.types.BMesh = bmesh.from_edit_mesh(shape.data)
selected_verts: list[bmesh.types.BMVert] = [vert for vert in bmesh_mesh.verts if vert.select == True]
i: int = 0
merged_vertices: dict[set[int],list[int]] = {} #make a list of sets which act as pairs. the pairs being sets means it doesn't matter if element 0 is at index 1, it is still considered the same pair
mergers: dict[bmesh.types.BMVert, bmesh.types.BMVert]
for name,mergers in bmesh.ops.find_doubles(bmesh_mesh,verts=selected_verts,dist=merge_distance).items():
for source_vert,target_vert in mergers.items():
pair: set[int] = set()
pair.add(source_vert.index)
pair.add(target_vert.index)
frozen_pair = frozenset(pair)
merged_vertices[frozen_pair] = [source_vert.index,target_vert.index] #put the pairs we have found into a list.
if(final_merged_vertex_group == []): #populate list if it is empty
final_merged_vertex_group = merged_vertices
new_dict: dict[set[int],list[int]] = {}
#update our final list, keeping pairs that exist on all shapekeys and not just one.
for key,value in final_merged_vertex_group.items():
if key in merged_vertices.keys():
new_dict[key] = value
final_merged_vertex_group = new_dict
#create an edit mesh and ensure it's vertex table
select_obj(context, mesh['mesh'], target_mode='EDIT')
data_mesh: bpy.types.Mesh = mesh['mesh'].data
mappings: dict[bmesh.types.BMVert,bmesh.types.BMVert] = {}
bmesh_mesh: bmesh.types.BMesh = bmesh.from_edit_mesh(data_mesh)
bmesh_mesh.verts.ensure_lookup_table()
#turn our pairs into a dictionary, which allows for merging vertices based on the shared pairs.
for key,value in final_merged_vertex_group.items():
mappings[bmesh_mesh.verts[value[0]]] = bmesh_mesh.verts[value[1]]
#weld the verts and update the source mesh
bmesh.ops.weld_verts(bmesh_mesh,targetmap=mappings)
bmesh.update_edit_mesh(data_mesh, destructive=True)
#delete the shapekey reading meshes.
for shape in mesh["shapekeys"]:
bpy.data.objects.remove(shape)
return {'RUNNING_MODAL'}
except Exception as e:
print(traceback.format_exception(e))
logger.error(f"Error in modal: {traceback.format_exception(e)}")
return {'CANCELLED'}