036e260dd6
- 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
175 lines
7.0 KiB
Python
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'}
|