# SPDX-License-Identifier: GPL-2.0-or-later
"""
Symmetry Tools: Find Asymmetric + Make Symmetric (by Sergey Sharybin)

Our character wasn’t completely symmetric in some parts where it was
supposed to, this could be by moving vertices by mistake or just reasons.
To fix this in a fast way, Sergey coded this two super useful tools:

* Find Asymmetric:
Selects vertices that don’t have the same position on the opposite side.

* Make Symmetric:
Move selected vertices to match the position of those on the other side.

This tools may not apply on every single model out there, but I tried it
in many different characters and it worked. So probably better use it on
those models that were already symmetric at some point, modeled with a
mirror modifier or so.
Search (spacebar) for "Find Asymmetric", and "Make Symmetric""Settings".

> Developed during Caminandes Open Movie Project
"""

import bpy
import bmesh
from mathutils import Vector


class AMTH_MESH_OT_find_asymmetric(bpy.types.Operator):

    """
    Find asymmetric vertices
    """

    bl_idname = "mesh.find_asymmetric"
    bl_label = "Find Asymmetric"
    bl_options = {"UNDO", "REGISTER"}

    @classmethod
    def poll(cls, context):
        object = context.object
        if object:
            return object.mode == "EDIT" and object.type == "MESH"
        return False

    def execute(self, context):
        threshold = 1e-6

        object = context.object
        bm = bmesh.from_edit_mesh(object.data)

        # Deselect all the vertices
        for v in bm.verts:
            v.select = False

        for v1 in bm.verts:
            if abs(v1.co[0]) < threshold:
                continue

            mirror_found = False
            for v2 in bm.verts:
                if v1 == v2:
                    continue
                if v1.co[0] * v2.co[0] > 0.0:
                    continue

                mirror_coord = Vector(v2.co)
                mirror_coord[0] *= -1
                if (mirror_coord - v1.co).length_squared < threshold:
                    mirror_found = True
                    break
            if not mirror_found:
                v1.select = True

        bm.select_flush_mode()

        bmesh.update_edit_mesh(object.data)

        return {"FINISHED"}


class AMTH_MESH_OT_make_symmetric(bpy.types.Operator):

    """
    Make symmetric
    """

    bl_idname = "mesh.make_symmetric"
    bl_label = "Make Symmetric"
    bl_options = {"UNDO", "REGISTER"}

    @classmethod
    def poll(cls, context):
        object = context.object
        if object:
            return object.mode == "EDIT" and object.type == "MESH"
        return False

    def execute(self, context):
        threshold = 1e-6

        object = context.object
        bm = bmesh.from_edit_mesh(object.data)

        for v1 in bm.verts:
            if v1.co[0] < threshold:
                continue
            if not v1.select:
                continue

            closest_vert = None
            closest_distance = -1
            for v2 in bm.verts:
                if v1 == v2:
                    continue
                if v2.co[0] > threshold:
                    continue
                if not v2.select:
                    continue

                mirror_coord = Vector(v2.co)
                mirror_coord[0] *= -1
                distance = (mirror_coord - v1.co).length_squared
                if closest_vert is None or distance < closest_distance:
                    closest_distance = distance
                    closest_vert = v2

            if closest_vert:
                closest_vert.select = False
                closest_vert.co = Vector(v1.co)
                closest_vert.co[0] *= -1
            v1.select = False

        for v1 in bm.verts:
            if v1.select:
                closest_vert = None
                closest_distance = -1
                for v2 in bm.verts:
                    if v1 != v2:
                        mirror_coord = Vector(v2.co)
                        mirror_coord[0] *= -1
                        distance = (mirror_coord - v1.co).length_squared
                        if closest_vert is None or distance < closest_distance:
                            closest_distance = distance
                            closest_vert = v2
                if closest_vert:
                    v1.select = False
                    v1.co = Vector(closest_vert.co)
                    v1.co[0] *= -1

        bm.select_flush_mode()
        bmesh.update_edit_mesh(object.data)

        return {"FINISHED"}


def ui_symmetry_tools(self, context):
    if bpy.context.mode == 'EDIT_MESH':
     self.layout.separator()
     self.layout.operator(
     AMTH_MESH_OT_find_asymmetric.bl_idname,
     icon="ALIGN_CENTER", text="Find Asymmetric")
     self.layout.operator(
     AMTH_MESH_OT_make_symmetric.bl_idname,
     icon="ALIGN_JUSTIFY", text="Make Symmetric")


def register():
    bpy.utils.register_class(AMTH_MESH_OT_find_asymmetric)
    bpy.utils.register_class(AMTH_MESH_OT_make_symmetric)
    bpy.types.VIEW3D_MT_edit_mesh.append(ui_symmetry_tools)


def unregister():
    bpy.utils.unregister_class(AMTH_MESH_OT_find_asymmetric)
    bpy.utils.unregister_class(AMTH_MESH_OT_make_symmetric)
    bpy.types.VIEW3D_MT_edit_mesh.remove(ui_symmetry_tools)
