Files
Avatar-Toolkit/functions/optimization/remove_doubles.py
T
989onan 036e260dd6 Vastly improve Merge Doubles
- removed advanced merge doubles, it just does advanced by default
- same behavior as advanced was before, but now completes the task in under a second. Thanks to the power of BMesh!
- Labels now reflect this change
2025-04-03 19:12:55 -04:00

175 lines
7.0 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"""
if(shapekey_name != ""):
mesh.active_shape_key_index = mesh.data.shape_keys.key_blocks.find(shapekey_name)
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()
if(shapekey_name != ""):
bpy.ops.object.shape_key_move(type='TOP')
bpy.ops.object.shape_key_remove(all=True,apply_mix=False)
duplicate = context.view_layer.objects.active
if(shapekey_name != ""):
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))]
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: set[int] = set()
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():
merged_vertices.add(source_vert.index)
merged_vertices.add(target_vert.index)
final_merged_vertex_group = [v for v in final_merged_vertex_group if v in merged_vertices]
select_obj(context, mesh['mesh'], target_mode='EDIT')
data_mesh: bpy.types.Mesh = mesh['mesh'].data
bmesh_mesh: bmesh.types.BMesh = bmesh.from_edit_mesh(data_mesh)
mergable_on_all_shapes: list[bmesh.types.BMVert] = [vert for vert in bmesh_mesh.verts if vert.index in final_merged_vertex_group]
mappings: dict[bmesh.types.BMVert,bmesh.types.BMVert] = bmesh.ops.find_doubles(bmesh_mesh,verts=mergable_on_all_shapes,dist=merge_distance)["targetmap"]
bmesh.ops.weld_verts(bmesh_mesh,targetmap=mappings)
bmesh.update_edit_mesh(data_mesh, destructive=True)
for shape in mesh["shapekeys"]:
bpy.data.objects.remove(shape)
return {'RUNNING_MODAL'}
except Exception as e:
logger.error(f"Error in modal: {traceback.format_exception(e)}")
return {'CANCELLED'}