# ***** BEGIN GPL LICENSE BLOCK *****
#
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.    See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ***** END GPL LICENCE BLOCK *****


bl_info = {
    "name": "Rotate Face",
    "author": "Jose Conseco",
    "version": (1, 0),
    "blender": (2, 80, 0),
    "location": "Mesh -> bottom of 'Face' menu",
    "description": "Rotate face while preserwing adjacent faces planes",
    "warning": "",
    "wiki_url": "",
    "category": "Mesh",
}

import bpy
import bmesh
import bpy_extras
from mathutils import Vector, Matrix, Quaternion
from mathutils.geometry import intersect_line_plane, distance_point_to_plane, normal

from bpy.types import Operator, GizmoGroup
from bpy.props import FloatVectorProperty

#TODO: detect intersection on moving out? Then lock plane???


def matrix_decompose(matrix_world):
    ''' returns active_obj_mat_loc, active_obj_mat_rot, active_obj_mat_sca 
        reconstruct by loc @ rotQuat @ scale '''

    loc, rotQuat, scale = matrix_world.decompose()

    active_obj_mat_loc = Matrix.Translation(loc)
    active_obj_mat_rot = rotQuat.to_matrix().to_4x4()
    active_obj_mat_sca = Matrix()
    for i in range(3):
        active_obj_mat_sca[i][i] = scale[i]

    return active_obj_mat_loc, active_obj_mat_rot, active_obj_mat_sca


def get_scale_mat(vec_scale):
    mat_scale = Matrix.Identity(4)
    mat_scale[0][0] = vec_scale.x
    mat_scale[1][1] = vec_scale.y
    mat_scale[2][2] = vec_scale.z
    return mat_scale

def get_bm_faces_center(bm):
    if isinstance(bm.select_history.active, bmesh.types.BMEdge):
        e = bm.select_history.active
        center = (e.verts[0].co+e.verts[1].co)/2
    else:
        if bm.faces.active:
            center = bm.faces.active.calc_center_median()
        else:
            center = Vector((0,0,0))
            sel_faces = [f for f in bm.faces if f.select]
            if sel_faces:
                for f in sel_faces:
                    center += f.calc_center_median()
                center = center / len(sel_faces)
    return center

def get_bm_tangent(bm):
    face_tan = bm.faces.active.calc_tangent_edge().normalized() #based on longest edge. TODO: remake to active edge?
    if bm.select_history.active and isinstance(bm.select_history.active, bmesh.types.BMEdge):
        face_tan = (bm.select_history.active.verts[0].co-bm.select_history.active.verts[1].co).normalized()
    return face_tan

def get_non_planar_edge(vert):
    ''' return edge with non parallel adjacent face normals '''
    link_edges = [e for e in vert.link_edges if not e.select]
    max_dot = 0.9
    best_edge = None
    for e in link_edges:
        if len(e.link_faces) > 1:
            dot = abs(e.link_faces[0].normal.dot(e.link_faces[1].normal))
            if dot < max_dot:
                max_dot = dot
                best_edge = e
    return best_edge

def get_counter_facing_edge(vert, plane_no):
    ''' return best edge most parallel plane_no '''
    min_dot = 0.1  # at least 1 deg  between 2 edges
    min_angle_edge = None
    other_edges = [e for e in vert.link_edges if not e.select]
    for other_edge in other_edges:
        vector_edge = vert.co - other_edge.other_vert(vert).co
        if vector_edge.length > 0.0001: # v.angle(v)  / dot wont work on 0 len vec
            dot = abs(vector_edge.normalized().dot(plane_no))
            if dot > min_dot:  # 90 deg fron normal ignore
                min_dot = dot
                min_angle_edge = other_edge
        else:
            min_angle_edge = other_edge
    return min_angle_edge


shared_data = {}
shared_data['init_face_no'] = None 


class RotateFace(Operator):
    """UV Operator description"""
    bl_idname = "mesh.rotate_face"
    bl_label = "Rotate Face"
    bl_options = {'REGISTER', 'UNDO'}

    plane_co: FloatVectorProperty(size=3, default=(0, 0, 0))
    plane_no: FloatVectorProperty(size=3, default=(0, 0, 1))
    bisect: bpy.props.BoolProperty(name='Bisect', description='', default=False)

    @classmethod
    def poll(cls, context):
        return (context.mode == 'EDIT_MESH')

    def invoke(self, context, event):
        obj = context.active_object
        me = obj.data
        bm = bmesh.from_edit_mesh(me)

        shared_data['T_R_G'] = (False, False, False)  # gizmo executed from translation, or red(x) rot, or green(y) rot
        shared_data['init_face_no'] = bm.faces.active.normal.copy()  # store init active face no
        shared_data['GIZMO_FROM_ROT'] = None  # are we running operator update from gizmo rotate widget?

        # if not self.properties.is_property_set("plane_co"):
        self.plane_co = get_bm_faces_center(bm)

        # if not self.properties.is_property_set("plane_no"):
        # if context.space_data.type == 'VIEW_3D':
        self.plane_no = bm.faces.active.normal
        face_tan = get_bm_tangent(bm)
        face_bi_tan = Vector(self.plane_no).cross(face_tan)

        shared_data['x_vec'] = face_tan.copy()
        shared_data['y_vec'] = face_bi_tan

        shared_data['plane_co'] = self.plane_co
        shared_data['plane_no'] = self.plane_no
        shared_data['is_bisect'] = False

        self.execute(context)

        if context.space_data.type == 'VIEW_3D':
            wm = context.window_manager
            wm.gizmo_group_type_ensure(RotateFaceGizmoGroup.bl_idname)
        bpy.ops.ed.undo_push()
        return {'FINISHED'}

    def execute(self, context):
        plane_co = Vector(self.plane_co)
        plane_no = Vector(self.plane_no).normalized()

        obj = context.active_object
        me = obj.data
        bm = bmesh.from_edit_mesh(me)
        if self.bisect:
            result = bmesh.ops.bisect_plane(bm, geom=bm.verts[:] + bm.edges[:] + bm.faces[:], plane_co=plane_co, plane_no=plane_no, clear_outer=True)
            edges = [e for e in result['geom_cut'] if isinstance(e, bmesh.types.BMEdge)]
            # result = bmesh.ops.holes_fill(bm, edges=edges)
            result = bmesh.ops.contextual_create(bm, geom=edges)
            
            if result['faces']:
                for f in bm.faces:
                    f.select = False
                for f in result['faces']:
                    f.select = True
                bm.faces.active = result['faces'][0]
                plane_co = result['faces'][0].calc_center_median()
                new_plane_normal = plane_no
            else:
                new_plane_normal = plane_no

        else:
            active_face = bm.faces.active
            sel_faces = [f for f in bm.faces if f.select]

            boundary_verts = list(set([v for f in sel_faces for v in f.verts]))
            #* get rail edges and loose verts (with no rail edges)
            rail_edges = []
            loose_verts = [] #werts that have no linked edges connected, or link edges are not valid
            for v in boundary_verts:
                adj_edges = [e for e in v.link_edges if not e.select]
                adj_edges_count = len(adj_edges)
                if adj_edges_count > 1:
                    best_edge = get_non_planar_edge(v) #edge with non parallel adjacent normals
                    if not best_edge:
                        best_edge = get_counter_facing_edge(v, plane_no)
                        if not best_edge:
                            loose_verts.append(v)
                        else:
                            rail_edges.append(best_edge)
                    else:
                        rail_edges.append(best_edge)
                elif adj_edges_count == 1:
                    #* check if only edge, is not parallel.
                    vector_edge = v.co - adj_edges[0].other_vert(v).co
                    if vector_edge.length > 0.0001 and abs(3.141/2 - vector_edge.angle(plane_no)) > 0.02:  # at least 1 deg
                        rail_edges.append(adj_edges[0])
                    else:
                        loose_verts.append(v)
                else: #no adj edges
                    loose_verts.append(v)

            #* now get colliding verts - below plane
            coliding_verts = []
            colision_protusion_size = []
            slice_is_flipped = False
            for e in rail_edges:
                intersection = intersect_line_plane(e.verts[0].co, e.verts[1].co, plane_co, plane_no, True)
                if not intersection:
                    continue
                for v in e.verts:
                    if not v.select:
                        #* is plane_no backfacing any rail edge dir?
                        v1_v2 = (e.other_vert(v).co - v.co).normalized()
                        # plane_dot_edge = plane_no.dot(v1_v2)  #! not true is some cases
                        # if plane_dot_edge < 0:  #* if face will give flipped result
                        #     # use last working normal
                        #     slice_is_flipped = True

                        #* is plane below intersecting edges?  If so seave those edges ver
                        v1_int = (intersection - v.co).normalized()
                        intersect_dot_edge = v1_int.dot(v1_v2)  # dot between
                        if intersect_dot_edge < 0.001: # cases - 1,2,3 collapsed edges.
                            # print(f'Coliding edge = {e.index}')
                            collision_protusion_len = (e.other_vert(v).co-intersection).project(plane_no).length
                            #collision_protusion_len = distance_point_to_plane(e.other_vert(v).co, plane_co, plane_no) #same
                            colision_protusion_size.append(collision_protusion_len)
                            coliding_verts.append(v.co)  # use other vert when projection is bad
            #* sort protuding verts by how much they stick out of plane
            coliding_verts = [vert for _, vert in sorted(zip(colision_protusion_size, coliding_verts), reverse=True)]  # the more outside of surface, then more condidate for plane vert

            #*  deal with 4 cases  - 0 , 1, 2, 3 collapsed edges
            collapsed_verts_count = len(coliding_verts)
            new_plane_normal = plane_no.copy()
            if collapsed_verts_count == 0:
                pass
            elif collapsed_verts_count == 1: #! what if after plane rotation other edges is intersected by new plane
                dist = distance_point_to_plane(coliding_verts[0], plane_co, plane_no)
                collapsed_vert_proj = coliding_verts[0] - plane_no * dist
                center_vert_vec = (coliding_verts[0] - plane_co)
                
                if shared_data['T_R_G'][1] == True:   # * version with locking y axis on rot x
                    center_v1_vec_y = center_vert_vec - center_vert_vec.project(shared_data['x_vec']) # center_vert_vec without x component
                    new_plane_normal = shared_data['x_vec'].cross(center_v1_vec_y).normalized()

                elif shared_data['T_R_G'][2] == True:  # * version with locking x axis on rot y
                    center_v1_vec_x = center_vert_vec - center_vert_vec.project(shared_data['y_vec'])  # center_vert_vec without y component
                    new_plane_normal = shared_data['y_vec'].cross(center_v1_vec_x).normalized()

                else: #* Ver with plane tilting for move
                    center_ver_proj_vec = collapsed_vert_proj - plane_co
                    rot_diff_quat = center_ver_proj_vec.rotation_difference(center_vert_vec)
                    new_plane_normal = plane_no.copy()
                    new_plane_normal.rotate(rot_diff_quat)  # changes inplace new_plane_normal

            elif (collapsed_verts_count == 2 and not shared_data['GIZMO_FROM_ROT']) or (collapsed_verts_count >= 2 and shared_data['GIZMO_FROM_ROT']):
                new_plane_normal = normal([coliding_verts[0], coliding_verts[1], plane_co])
                new_plane_normal = new_plane_normal if new_plane_normal.dot(plane_no) > 0 else -1*new_plane_normal
            elif collapsed_verts_count >= 3:
                new_plane_normal = normal([coliding_verts[0], coliding_verts[1], coliding_verts[2]])
                new_plane_normal = new_plane_normal if new_plane_normal.dot(plane_no) > 0 else -1*new_plane_normal
                plane_co = (coliding_verts[0] + coliding_verts[1] + coliding_verts[2])/3

            
            for e in rail_edges:
                intersection = intersect_line_plane(e.verts[0].co, e.verts[1].co, plane_co, new_plane_normal, True)  # use only y part from fixed_norm
                if not intersection:
                    continue
                for v in e.verts:
                    if v.select:
                        # dot_v = (intersection - e.other_vert(v)).dot(v - e.other_vert(v))  # dot between
                        v.co = intersection
                        
                        # if dot_v > 0.001:  # cases - 1,2,3 collapsed edges.
                            # coliding_verts.append(e.other_vert(v))  # use other vert when projection is bad
            for v in loose_verts: #with not valid linked slide edges
                intersection = intersect_line_plane(v.co, v.co+new_plane_normal, plane_co, new_plane_normal, True)
                v.co = intersection

            for e in rail_edges:
                if (e.verts[0].co - e.verts[1].co).length < 0.001:  # if close together weld them at not selected vert
                    v1 = e.verts[0] if not e.verts[0].select else e.verts[1]
                    v2 = e.other_vert(v1)
                    v1.select = True
                    bmesh.ops.pointmerge(bm, verts=[v1, v2], merge_co=v2.co)

        bm.select_flush(True) #fixes selected faces after above
        # for f in sel_faces:
        #     f.normal_update()
        bm.normal_update()

        shared_data['plane_co'] = plane_co
        shared_data['plane_no'] = new_plane_normal
        shared_data['is_bisect'] = self.bisect

        bmesh.update_edit_mesh(me, True)
        bpy.ops.ed.undo_push()
        return {'FINISHED'}


# Gizmos for plane_co, plane_no
class RotateFaceGizmoGroup(GizmoGroup):
    bl_idname = "MESH_GGT_rotate_face"
    bl_label = "Rotate face Gizmo"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'WINDOW'
    bl_options = {'3D'}

    # Helper functions
    @staticmethod
    def my_target_operator(context):
        wm = context.window_manager
        op = wm.operators[-1] if wm.operators else None
        if isinstance(op, RotateFace):
            return op
        return None

    @staticmethod
    def my_view_orientation(context):
        rv3d = context.space_data.region_3d
        view_ma = rv3d.view_matrix
        return view_ma

    # @classmethod
    # def poll(cls, context):
    #     op = cls.my_target_operator(context)
    #     if op is None:
    #         wm = context.window_manager
    #         wm.gizmo_group_type_unlink_delayed(RotateFaceGizmoGroup.bl_idname)
    #         return False
    #     return True

    def setup(self, context):
        # ----
        # Move
        obj = context.active_object
        obj_scale = obj.scale.length
        self.offset = 0
        self.rot_x = 0
        self.rot_y = 0
        self.obj = obj
        self.mw = obj.matrix_world.copy()
        self.obj_mat_loc, self.obj_mat_rot, self.obj_mat_sca = matrix_decompose(self.mw)

        # self.scale_fix = Matrix.Indentity(4)
        # self.scale_fix = 1 if self.obj_mat_sca[0] > 0 else -1   # x
        # self.scale_fix = 1 if self.obj_mat_sca[1] > 0 else -1   # y
        # self.scale_fix = 1 if self.obj_mat_sca[2] > 0 else -1   # z

        me = obj.data
        self.mw_inv = self.mw.inverted()
        self.mw_norm = self.mw.normalized() #sets scale to 1
        self.mw_norm_inv = self.mw_norm.inverted() # sets scale to (+/1) one, while preserving visual rotation (changes rot mat)

        self.rot_dir_flip = 1 if self.mw_norm_inv[0][0] * self.mw_norm_inv[1][1] * self.mw_norm_inv[2][2] > 0 else -1 

        bm = bmesh.from_edit_mesh(me)
        self.first_run = True
        self.face_no = bm.faces.active.normal.copy()

        self.center = get_bm_faces_center(bm)
        self.init_center_world = self.mw @ self.center

        face_tan = get_bm_tangent(bm)
        face_bi_tan = self.face_no.cross(face_tan)

        self.last_plane_no = self.face_no.copy()
        self.last_plane_co = self.center.copy()

        self.last_tangent = face_tan
        self.last_bitangent = face_bi_tan

        shared_data['x_vec'] = face_bi_tan.copy()
        shared_data['y_vec'] = face_tan.copy()

        def move_get_cb():
            loc_current = Vector(self.widget_move.matrix_basis.col[3].xyz) #will be broken cos of scaling self.coneter by sca_mat - fixes gizmo  draw proportions
            arrow_normal = Vector(self.widget_move.matrix_basis.col[2].xyz)
            old_to_new_vec = self.center - Vector(self.last_plane_co)
            arrow_offset = old_to_new_vec.length
            # sign = -1 if arrow_normal.dot(Vector(old_to_new_vec)) > 0 else 1
            # return sign * arrow_offset
            return self.offset

        def move_set_cb(value):
            # op = RotateFaceGizmoGroup.my_target_operator(context)
            # op.execute(context)
            normal = self.mw_norm_inv.to_3x3() @ Vector(self.widget_move.matrix_basis.col[2].xyz)
            shared_data['plane_co'] = value * normal + self.last_plane_co
            # self.widget_move.matrix_offset.col[3].xyz = shared_data['plane_co']
            self.offset = value
            if self.first_run:
                self.first_run = False
            else:
                bpy.ops.ed.undo()

            shared_data['GIZMO_FROM_ROT'] = False
            shared_data['T_R_G'] = (True, False, False)
            bpy.ops.mesh.rotate_face(plane_co=Vector(shared_data['plane_co']), plane_no=Vector(shared_data['plane_no']), bisect=shared_data['is_bisect'])
            
        widget_move = self.gizmos.new("GIZMO_GT_arrow_3d")
        widget_move.target_set_handler("offset", get=move_get_cb, set=move_set_cb)
        widget_move.use_draw_value = True

        widget_move.color = 0.8, 0.8, 0.8
        widget_move.alpha = 0.8

        widget_move.color_highlight = 1.0, 1.0, 1.0
        widget_move.alpha_highlight = 1.0
        widget_move.line_width = 3.

        widget_move.scale_basis = 1.5

        self.widget_move = widget_move

        # ----
        # Dial red
        import math
        def rot_get_red():
            # loc, current_rotQuat, scale = self.widget_rot_red.matrix_basis.decompose()
            # rot_x = self.init_rot_x - current_rotQuat.to_euler().x
            # return self.rot_x
            return 0
            

        def rot_set_red(value):
            self.rot_x = value

            rot_red_z = self.mw_norm_inv.to_3x3() @ Vector(self.widget_rot_red.matrix_basis.col[2].xyz)
            matrix_rot_x = Matrix.Rotation(-value * self.rot_dir_flip, 3, rot_red_z)

            rot_green_z = self.mw_norm_inv.to_3x3() @ Vector(self.widget_rot_green.matrix_basis.col[2].xyz)
            matrix_rot_y = Matrix.Rotation(-self.rot_y, 3, rot_green_z)

            shared_data['plane_no'] = matrix_rot_y @ matrix_rot_x @ self.last_plane_no
            shared_data['plane_co'] = self.last_plane_co
            if self.first_run:
                self.first_run = False
            else:
                bpy.ops.ed.undo()
            shared_data['GIZMO_FROM_ROT'] = True
            shared_data['T_R_G'] = (False, True, False)
            shared_data['x_vec'] = matrix_rot_y @ matrix_rot_x @ self.last_bitangent
            shared_data['y_vec'] = matrix_rot_y @ matrix_rot_x @ self.last_tangent
            bpy.ops.mesh.rotate_face(plane_co=Vector(shared_data['plane_co']), plane_no=Vector(shared_data['plane_no']), bisect=shared_data['is_bisect'])

        widget_rot_red = self.gizmos.new("GIZMO_GT_dial_3d")
        widget_rot_red.target_set_handler("offset", get=rot_get_red, set=rot_set_red)
        widget_rot_red.draw_options = {'ANGLE_START_Y', 'ANGLE_VALUE', 'FILL_SELECT'}
        # widget_rot_red.properties.wrap_angle = False
        widget_rot_red.use_draw_value = True

        widget_rot_red.color = 0.8, 0.0, 0.0
        widget_rot_red.alpha = 0.8
        widget_rot_red.line_width = 2.5
        widget_rot_red.scale_basis = 0.75

        widget_rot_red.color_highlight = 1.0, 0.0, 0.0
        widget_rot_red.alpha_highlight = 1.0

        self.widget_rot_red = widget_rot_red

        # ----
        # Dial green

        def rot_get_green():
            # loc, rotQuat, scale = self.widget_rot_green.matrix_basis.decompose()
            # return self.init_rot_y - rotQuat.to_euler().y
            # return self.rot_y
            return 0

        #todo: fix orientation when negative obj scale.
        def rot_set_green(value):
            self.rot_y = value

            rot_red_z = self.mw_norm_inv.to_3x3() @ Vector(self.widget_rot_red.matrix_basis.col[2].xyz)
            matrix_rot_x = Matrix.Rotation(-self.rot_x, 3, rot_red_z)

            rot_green_z = self.mw_norm_inv.to_3x3() @ Vector(self.widget_rot_green.matrix_basis.col[2].xyz)
            matrix_rot_y = Matrix.Rotation(-self.rot_y * self.rot_dir_flip, 3, rot_green_z)

            shared_data['plane_no'] = matrix_rot_y @ matrix_rot_x @ self.last_plane_no
            shared_data['plane_co'] = self.last_plane_co
            if self.first_run:
                self.first_run = False
            else:
                bpy.ops.ed.undo()
            shared_data['GIZMO_FROM_ROT'] = True
            shared_data['T_R_G'] = (False, False, True)
            shared_data['x_vec'] = matrix_rot_y @ matrix_rot_x @ self.last_bitangent
            shared_data['y_vec'] = matrix_rot_y @ matrix_rot_x @ self.last_tangent
            bpy.ops.mesh.rotate_face(plane_co=Vector(shared_data['plane_co']), plane_no=Vector(shared_data['plane_no']), bisect=shared_data['is_bisect'])

        widget_rot_green = self.gizmos.new("GIZMO_GT_dial_3d")
        widget_rot_green.target_set_handler("offset", get=rot_get_green, set=rot_set_green)
        widget_rot_green.draw_options = {'ANGLE_START_Y', 'ANGLE_VALUE', 'FILL_SELECT'}
        widget_rot_green.use_draw_value = True

        widget_rot_green.color = 0.0, 0.8, 0.0
        widget_rot_green.alpha = 0.8
        widget_rot_green.line_width = 2.5
        widget_rot_green.scale_basis = 0.75

        widget_rot_green.color_highlight = 0.0, 1.0, 0.0
        widget_rot_green.alpha_highlight = 1.0

        self.widget_rot_green = widget_rot_green

        button_bisect = self.gizmos.new("GIZMO_GT_button_2d")   # This line crashes Blender
        # button_bisect.matrix_space = context.object.matrix_world
        button_bisect.target_set_operator("object.bisect_switch")
        button_bisect.icon = 'SELECT_SUBTRACT'
        button_bisect.draw_options = {'BACKDROP', 'OUTLINE', 'HELPLINE'}

        button_bisect.alpha = 0.0
        button_bisect.color_highlight = 0.8, 0.8, 0.8
        button_bisect.alpha_highlight = 0.2

        button_bisect.scale_basis = 0.2  # Same as buttons defined in C
        self.button_bisect = button_bisect

    def refresh(self, context):
        op = self.my_target_operator(context)
        
        wm = context.window_manager
        op = wm.operators[-1] if wm.operators else None
        if (op and not isinstance(op, RotateFace)) or context.active_object.mode != 'EDIT':
            # if op is None:
            bpy.context.window_manager.gizmo_group_type_unlink_delayed(RotateFaceGizmoGroup.bl_idname)
            return

        
            
        me = self.obj.data
        bm = bmesh.from_edit_mesh(me)
        face_no = bm.faces.active.normal.copy()

        center = get_bm_faces_center(bm)
        face_tan = get_bm_tangent(bm)
        face_bi_tan = face_no.cross(face_tan)
        # mw @ center = mw_norm @ mat_fix @ center
        # mw_norm.inv @ mw = mat_fix

        # center_fixed = self.obj_mat_sca @ center  # self.obj_mat_rot @
        center_location_fix = self.mw_norm_inv @ self.mw
        center_fixed = center_location_fix @ center  # no scale on  gizmo, but scale/move plane_co

        # view_ma = self.my_view_orientation(context)
        # rotate_axis = view_ma[2].xyz
        # screen_up = view_ma[1].xyz
        # screen_right = view_ma[0].xyz
        
        # = trans_mat @ trans_rot_mat #! keeps the offset
        mb = Matrix.Identity(4)
        mb.col[0].xyz = face_bi_tan
        mb.col[1].xyz = face_tan
        mb.col[2].xyz = face_no
        mb.col[3].xyz = center_fixed
        mb = self.mw_norm @ mb
        self.widget_move.matrix_basis = mb

        mb = Matrix.Identity(4)
        mb.col[0].xyz = face_no # rot X
        mb.col[1].xyz = face_tan
        mb.col[2].xyz = face_bi_tan
        mb.col[3].xyz = center_fixed
        mb = self.mw_norm @ mb
        self.widget_rot_red.matrix_basis = mb  # = trans_mat @ red_rot_mat

        mb = Matrix.Identity(4)
        mb.col[0].xyz = face_bi_tan
        mb.col[1].xyz = face_no  # rot Y
        mb.col[2].xyz = face_tan
        mb.col[3].xyz = center_fixed
        mb = self.mw_norm @ mb
        self.widget_rot_green.matrix_basis = mb  # = trans_mat @ red_rot_mat

        # loc, rotQuat, scale = mb.decompose()
        # self.init_rot_y = rotQuat.to_euler().y

        self.offset = 0
        self.rot_x = 0
        self.rot_y = 0

        self.last_plane_no = face_no
        self.last_plane_co = center #use edge as new center
        self.last_tangent = face_tan
        self.last_bitangent = face_bi_tan
        print('REFERSH')


    def draw_prepare(self, context):
        if shared_data['is_bisect']:
            self.widget_move.color = 0.9, 0.6, 0.2
            self.widget_move.color_highlight = 1.0, 0.7, 0.3
        else:
            self.widget_move.color = 0.8, 0.8, 0.8
            self.widget_move.color_highlight = 1.0, 1.0, 1.0

        #* for  button_bisect - bisect only...
        view_ma = self.my_view_orientation(context)

        self.view_mat = view_ma
        rotate_axis = view_ma[2].xyz
        screen_up = view_ma[1].xyz
        screen_right = view_ma[1].xyz

        # self.button_bisect.matrix_basis = view_ma.inverted()
        mat = self.button_bisect.matrix_basis #= self.mw
        # mat = self.button_bisect.matrix_basis
        mat.identity()
        no_z = rotate_axis
        no_y = screen_up
        no_x = screen_right

        region = context.region
        rv3d = context.region_data

        location_2d = bpy_extras.view3d_utils.location_3d_to_region_2d(region, rv3d, self.init_center_world)
        circle1_offset = Vector((location_2d.x - 120, location_2d.y - 140))
        button_bisect_loc = bpy_extras.view3d_utils.region_2d_to_location_3d(region, rv3d, circle1_offset, self.init_center_world)
        mat.col[3].xyz = button_bisect_loc

        # print(f'Basis: {self.widget_rot_red.matrix_basis}')
        # print(f'Offset: {self.widget_rot_red.matrix_offset}')
        # print(f'Space: {self.widget_rot_red.matrix_space}')
        # print(f'Wrold: {self.widget_rot_red.matrix_world}')



class OBJECT_OT_BisectSwitch(bpy.types.Operator):
    bl_idname = "object.bisect_switch"
    bl_label = "Bisect Toggle"
    bl_description = "Bisect Toggle"
    bl_options = {"REGISTER", "UNDO", 'INTERNAL'}

    def execute(self, context):
        obj = context.active_object
        shared_data['is_bisect'] = not shared_data['is_bisect']
        # bpy.ops.mesh.rotate_face(plane_co=Vector(shared_data['plane_co']), plane_no=Vector(shared_data['plane_no']), bisect=shared_data['is_bisect'])
        return {"FINISHED"}

classes = (
    OBJECT_OT_BisectSwitch,
    RotateFace,
    RotateFaceGizmoGroup,
)


def draw_rot_face(self, context):
    self.layout.separator()
    self.layout.operator('mesh.rotate_face')
    

def register():
    for cls in classes:
        bpy.utils.register_class(cls)
    bpy.types.VIEW3D_MT_edit_mesh_faces.append(draw_rot_face)


def unregister():
    bpy.types.VIEW3D_MT_edit_mesh_faces.remove(draw_rot_face)
    for cls in reversed(classes):
        bpy.utils.unregister_class(cls)


if __name__ == "__main__":
    register()
