Skip to content
Snippets Groups Projects
xedit_free_rotate.py 57.4 KiB
Newer Older
  • Learn to ignore specific revisions
  • NBurn's avatar
    NBurn committed
    '''
    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 LICENSE BLOCK
    '''
    
    from copy import deepcopy
    from math import degrees, radians, pi
    
    import bpy
    import bmesh
    import bgl
    import blf
    
    import gpu
    from mathutils import geometry, Euler, Matrix, Quaternion, Vector
    
    NBurn's avatar
    NBurn committed
    from bpy_extras import view3d_utils
    from bpy_extras.view3d_utils import location_3d_to_region_2d as loc3d_to_reg2d
    from bpy_extras.view3d_utils import region_2d_to_vector_3d as reg2d_to_vec3d
    from bpy_extras.view3d_utils import region_2d_to_location_3d as reg2d_to_loc3d
    from bpy_extras.view3d_utils import region_2d_to_origin_3d as reg2d_to_org3d
    
    from gpu_extras.batch import batch_for_shader
    
    NBurn's avatar
    NBurn committed
    
    # "Constant" values
    (
        X,
        Y,
        Z,
    
        CLICK_CHECK,
        WAIT_FOR_POPUP,
        GET_0_OR_180,
        DO_TRANSFORM,
    
        MOVE,
        SCALE,
        ROTATE,
    ) = range(10)
    
    # globals
    curr_meas_stor = 0.0
    new_meas_stor = None
    popup_active = False
    
    
    #print("Loaded add-on.\n")  # debug
    
    
    class Colr:
        red    = 1.0, 0.0, 0.0, 0.6
        green  = 0.0, 1.0, 0.0, 0.6
        blue   = 0.0, 0.0, 1.0, 0.6
        white  = 1.0, 1.0, 1.0, 1.0
        grey   = 1.0, 1.0, 1.0, 0.4
        black  = 0.0, 0.0, 0.0, 1.0
        yellow = 1.0, 1.0, 0.0, 0.6
    
    
    
    class TransDat:
    
        '''
        Transformation Data
        values stored here get used for rotation
        '''
    
    NBurn's avatar
    NBurn committed
        placeholder = True
    
    
    
    def set_transform_data_none():
        TransDat.piv_norm = None  # Vector
        TransDat.new_ang_r = None
        TransDat.ang_diff_r = None  # float
        TransDat.axis_lock = None  # 'X', 'Y', 'Z'
        TransDat.lock_pts = None
        TransDat.rot_pt_pos = None
        TransDat.rot_pt_neg = None
        TransDat.arc_pts = None
    
    
    
    NBurn's avatar
    NBurn committed
    def editmode_refresh():
    
        '''
        Refreshes mesh drawing in 3D view and updates mesh coordinate
        data so ref_pts are drawn at correct locations.
        Using editmode_toggle to do this seems hackish, but editmode_toggle seems
        to be the only thing that updates both drawing and coordinate info.
        '''
    
    NBurn's avatar
    NBurn committed
        if bpy.context.mode == "EDIT_MESH":
            bpy.ops.object.editmode_toggle()
            bpy.ops.object.editmode_toggle()
    
    
    def backup_blender_settings():
        backup = [
            deepcopy(bpy.context.tool_settings.use_snap),
    
            deepcopy(bpy.context.tool_settings.snap_elements),
    
    NBurn's avatar
    NBurn committed
            deepcopy(bpy.context.tool_settings.snap_target),
    
            deepcopy(bpy.context.tool_settings.transform_pivot_point),
            deepcopy(bpy.context.scene.transform_orientation_slots[0].type),
            deepcopy(bpy.context.space_data.show_gizmo),
    
            deepcopy(bpy.context.scene.cursor.location)]
    
    NBurn's avatar
    NBurn committed
        return backup
    
    
    def init_blender_settings():
        bpy.context.tool_settings.use_snap = False
    
        bpy.context.tool_settings.snap_elements = {'VERTEX'}
    
    NBurn's avatar
    NBurn committed
        bpy.context.tool_settings.snap_target = 'CLOSEST'
    
        bpy.context.tool_settings.transform_pivot_point = 'ACTIVE_ELEMENT'
        bpy.context.scene.transform_orientation_slots[0].type = 'GLOBAL'
        bpy.context.space_data.show_gizmo = False
    
    NBurn's avatar
    NBurn committed
        return
    
    
    def restore_blender_settings(backup):
        bpy.context.tool_settings.use_snap = deepcopy(backup[0])
    
        bpy.context.tool_settings.snap_elements = deepcopy(backup[1])
    
    NBurn's avatar
    NBurn committed
        bpy.context.tool_settings.snap_target = deepcopy(backup[2])
    
        bpy.context.tool_settings.transform_pivot_point = deepcopy(backup[3])
        bpy.context.scene.transform_orientation_slots[0].type = deepcopy(backup[4])
        bpy.context.space_data.show_gizmo = deepcopy(backup[5])
    
        bpy.context.scene.cursor.location = deepcopy(backup[6])
    
    NBurn's avatar
    NBurn committed
        return
    
    
    def flts_alm_eq(flt_a, flt_b):
        tol = 0.0001
        return flt_a > (flt_b - tol) and flt_a < (flt_b + tol)
    
    
    
    # assume both float lists are same size?
    def flt_lists_alm_eq(ls_a, ls_b, tol=0.001):
        for i in range(len(ls_a)):
            if not (ls_a[i] > (ls_b[i] - tol) and ls_a[i] < (ls_b[i] + tol)):
                return False
        return True
    
    
    
    NBurn's avatar
    NBurn committed
    # todo : replace with flt_lists_alm_eq?
    def vec3s_alm_eq(vec_a, vec_b):
        X, Y, Z = 0, 1, 2
        if flts_alm_eq(vec_a[X], vec_b[X]):
            if flts_alm_eq(vec_a[Y], vec_b[Y]):
                if flts_alm_eq(vec_a[Z], vec_b[Z]):
                    return True
        return False
    
    
    class MenuStore:
        def __init__(self):
            self.cnt = 0
            self.active = 0  # unused ?
            # todo : replace above with self.current ?
            self.txtcolrs = []
            self.tcoords = []
            self.texts = []
            self.arrows = []  # arrow coordinates
    
    
    class MenuHandler:
        def __init__(self, title, tsize, act_colr, dis_colr, toolwid, reg):
    
            self.dpi = bpy.context.preferences.system.dpi
    
    NBurn's avatar
    NBurn committed
            self.title = title
            # todo : better solution than None "magic numbers"
            self.menus = [None]  # no menu for 0
            self.menu_cnt = len(self.menus)
            self.current = 0  # current active menu
            self.tsize = tsize  # text size
            self.act_colr = act_colr
            self.dis_colr = dis_colr  # disabled color
            self.reg = reg  # region
    
    
            self.view_offset = 20, 95  # box left top start
    
    NBurn's avatar
    NBurn committed
            self.box_y_pad = 8  # vertical space between boxes
    
            fontid = 0
            blf.size(fontid, tsize, self.dpi)
            lcase_wid, lcase_hgt = blf.dimensions(fontid, "n")
            ucase_wid, ucase_hgt = blf.dimensions(fontid, "N")
            bot_space = blf.dimensions(fontid, "gp")[1] - lcase_hgt
    
            self.full_txt_hgt = blf.dimensions(fontid, "NTgp")[1]
    
    NBurn's avatar
    NBurn committed
    
            arr_wid, arr_hgt = 12, 16
            arrow_base = (0, 0), (0, arr_hgt), (arr_wid, arr_hgt/2)
    
            aw_adj, ah_adj = arr_wid * 0.50, (arr_hgt - ucase_hgt) / 2
    
    NBurn's avatar
    NBurn committed
            self.arrow_pts = []
            for a in arrow_base:
                self.arrow_pts.append((a[0] - aw_adj, a[1] - ah_adj))
    
    
            self.blef = self.view_offset[0] + toolwid  # box left start
            #self.titlco = self.blef // 2, self.reg.height - self.view_offset[1]
            self.titlco = self.blef, self.reg.height - self.view_offset[1]
            self.btop = self.titlco[1] - (self.full_txt_hgt // 1.5)
    
    NBurn's avatar
    NBurn committed
            self.txt_y_pad = bot_space * 2
    
        def add_menu(self, strings):
            self.menus.append(MenuStore())
            new = self.menus[-1]
            btop = self.btop
            tlef = self.blef  # text left
            new.cnt = len(strings)
            for i in range(new.cnt):
                new.txtcolrs.append(self.dis_colr)
                new.texts.append(strings[i])
    
                bbot = btop - self.full_txt_hgt
                new.tcoords.append((tlef + self.view_offset[0], bbot))
    
    NBurn's avatar
    NBurn committed
                btop = bbot - self.box_y_pad
                new.arrows.append((
                    (self.arrow_pts[0][0] + tlef, self.arrow_pts[0][1] + bbot),
                    (self.arrow_pts[1][0] + tlef, self.arrow_pts[1][1] + bbot),
                    (self.arrow_pts[2][0] + tlef, self.arrow_pts[2][1] + bbot)))
            new.txtcolrs[new.active] = self.act_colr
            self.menu_cnt += 1
    
        def update_active(self, change):
            menu = self.menus[self.current]
            if menu is None:
                return
            menu.txtcolrs[menu.active] = self.dis_colr
            menu.active = (menu.active + change) % menu.cnt
            menu.txtcolrs[menu.active] = self.act_colr
    
        def change_menu(self, new):
            self.current = new
    
        def get_mode(self):
            menu = self.menus[self.current]
            return menu.texts[menu.active]
    
        #def rebuild_menus(self)  # add in case blender window size changes?
        #    return
    
        def draw(self, menu_visible):
            menu = self.menus[self.current]
            # prepare to draw text
            font_id = 0
            blf.size(font_id, self.tsize, self.dpi)
            # draw title
    
            #bgl.glColor4f(*self.dis_colr)
    
    NBurn's avatar
    NBurn committed
            blf.position(font_id, self.titlco[0], self.titlco[1], 0)
    
            blf.color(font_id, *self.dis_colr)
    
    NBurn's avatar
    NBurn committed
            blf.draw(font_id, self.title)
            # draw menu
            if menu_visible and menu is not None:
                for i in range(menu.cnt):
    
                    #bgl.glColor4f(*menu.txtcolrs[i])
    
    NBurn's avatar
    NBurn committed
                    blf.position(font_id, menu.tcoords[i][0], menu.tcoords[i][1], 0)
    
                    blf.color(font_id, *menu.txtcolrs[i])
    
    NBurn's avatar
    NBurn committed
                    blf.draw(font_id, menu.texts[i])
    
                # draw arrow
    
    NBurn's avatar
    NBurn committed
                bgl.glEnable(bgl.GL_BLEND)
                bgl.glColor4f(*self.act_colr)
                bgl.glBegin(bgl.GL_LINE_LOOP)
                for p in menu.arrows[menu.active]:
                    bgl.glVertex2f(*p)
                bgl.glEnd()
    
                '''
                indices = ((0, 1), (1, 2), (2, 0))
                shader = gpu.shader.from_builtin('2D_UNIFORM_COLOR')
                batch = batch_for_shader(shader, 'LINES', {"pos": menu.arrows[menu.active]}, indices=indices)
                shader.bind()
                shader.uniform_float("color", self.act_colr)
                batch.draw(shader)
    
    NBurn's avatar
    NBurn committed
    
    
    # === 3D View mouse location and button code ===
    class ViewButton():
        def __init__(self, colr_on, colr_off, txt_sz, txt_colr, offs=(0, 0)):
    
            self.dpi = bpy.context.preferences.system.dpi
    
    NBurn's avatar
    NBurn committed
            self.is_drawn = False
            self.ms_over = False  # mouse over button
            self.wid = 0
            self.coords = None
            #self.co_outside_btn = None
            self.co2d = None
            self.colr_off = colr_off  # colr when mouse not over button
            self.colr_on = colr_on  # colr when mouse over button
            self.txt = ""
            self.txt_sz = txt_sz
            self.txt_colr = txt_colr
            self.txt_co = None
            self.offset = Vector(offs)
    
            # Set button height and text offsets (to determine where text would
            # be placed within button). Done in __init__ as this will not change
            # during program execution and prevents having to recalculate these
            # values every time text is changed.
            font_id = 0
            blf.size(font_id, self.txt_sz, self.dpi)
            samp_txt_max = "Tgp"  # text with highest and lowest pixel values
            x, max_y =  blf.dimensions(font_id, samp_txt_max)
            y = blf.dimensions(font_id, "T")[1]  # T = sample text
            y_diff = (max_y - y)
    
            self.hgt = int(max_y + (y_diff * 2))
            self.txt_x_offs = int(x / (len(samp_txt_max) * 2) )
            self.txt_y_offs = int(( self.hgt - y) / 2) + 1
            # added 1 to txt_y_offs to compensate for possible int rounding
    
        # replace text string and update button width
        def set_text(self, txt):
            font_id = 0
            self.txt = txt
            blf.size(font_id, self.txt_sz, self.dpi)
            w = blf.dimensions(font_id, txt)[0]  # get text width
            self.wid = w + (self.txt_x_offs * 2)
            return
    
        def set_btn_coor(self, co2d):
            #offs_2d = Vector((-self.wid / 2, 25))
            offs_2d = Vector((-self.wid / 2, 0))
            new2d = co2d + offs_2d
    
    NBurn's avatar
    NBurn committed
            # co_bl == coordinate bottom left, co_tr == coordinate top right
            co_bl = new2d[0], new2d[1]
            co_tl = new2d[0], new2d[1] + self.hgt
            co_tr = new2d[0] + self.wid, new2d[1] + self.hgt
            co_br = new2d[0] + self.wid, new2d[1]
            self.coords = co_bl, co_tl, co_tr, co_br
            self.txt_co = new2d[0] + self.txt_x_offs, new2d[1] + self.txt_y_offs
            self.ms_chk = co_bl[0], co_tr[0], co_bl[1], co_tr[1]
    
        def pt_inside_btn2(self, mouse_co):
            mx, my = mouse_co[0], mouse_co[1]
            if mx < self.ms_chk[0] or mx > self.ms_chk[1]:
                return False
            if my < self.ms_chk[2] or my > self.ms_chk[3]:
                return False
            return True
    
    NBurn's avatar
    NBurn committed
        def draw_btn(self, btn_loc, mouse_co, highlight_mouse=False):
            if btn_loc is not None:
                offs_loc = btn_loc + self.offset
                font_id = 0
                colr = self.colr_off
                self.set_btn_coor(offs_loc)
                if self.pt_inside_btn2(mouse_co):
                    colr = self.colr_on
                    self.ms_over = True
                else:
                    self.ms_over = False
                # draw button box
    
    NBurn's avatar
    NBurn committed
                bgl.glColor4f(*colr)
                bgl.glBegin(bgl.GL_LINE_STRIP)
                for coord in self.coords:
                    bgl.glVertex2f(coord[0], coord[1])
                bgl.glVertex2f(self.coords[0][0], self.coords[0][1])
                bgl.glEnd()
    
                '''
                indc = ((0, 1), (1, 2), (2, 3), (3, 0))
                shader = gpu.shader.from_builtin('2D_UNIFORM_COLOR')
                batch = batch_for_shader(shader, 'LINES', {"pos": self.coords}, indices=indc)
                shader.bind()
                shader.uniform_float("color", colr)
                batch.draw(shader)
    
    
    NBurn's avatar
    NBurn committed
                # draw outline around button box
                if highlight_mouse and self.ms_over:
    
                    #bgl.glColor4f(*self.colr_off)
    
    NBurn's avatar
    NBurn committed
                    HO = 4  # highlight_mouse offset
                    offs = (-HO, -HO), (-HO, HO), (HO, HO), (HO, -HO)
    
                    #bgl.glBegin(bgl.GL_LINE_STRIP)
                    off_co = []
    
    NBurn's avatar
    NBurn committed
                    for i, coord in enumerate(self.coords):
    
                        off_co.append((coord[0] + offs[i][0], coord[1] + offs[i][1]))
                    off_co.append((self.coords[0][0] + offs[0][0], self.coords[0][1] + offs[0][1]))
    
                    shader = gpu.shader.from_builtin('2D_UNIFORM_COLOR')
                    batch = batch_for_shader(shader, 'LINES', {"pos": off_co})
                    shader.bind()
                    shader.uniform_float("color", self.colr_off)
                    batch.draw(shader)
    
    
    NBurn's avatar
    NBurn committed
                # draw button text
                blf.position(font_id, self.txt_co[0], self.txt_co[1], 0)
    
                #bgl.glColor4f(*self.txt_colr)
                blf.size(font_id, self.txt_sz, self.dpi)
                blf.color(font_id, *self.txt_colr)
                #blf.position(font_id, self.txt_co[0], self.txt_co[1], 0)
    
    NBurn's avatar
    NBurn committed
                blf.draw(font_id, self.txt)
    
    NBurn's avatar
    NBurn committed
            else:
                self.ms_over = False
    
    
    # Used for mod_pt mode
    class TempPoint():
        def __init__(self):
            self.ls = []  # point list
            self.cnt = 0
            self.co3d = None
            self.max_cnt = 50
    
        def average(self):
            vsum = Vector()
            for p in self.ls:
                vsum += p
            self.co3d = vsum / self.cnt
    
        def find_pt(self, co3d):
            found_idx = None
            for i in range(self.cnt):
                if self.ls[i] == co3d:
                    found_idx = i
                    break
            return found_idx
    
        def rem_pt(self, idx):
            self.ls.pop(idx)
            self.cnt -= 1
            if self.cnt > 0:
                self.average()
            else:
                self.co3d = None
    
        def try_add(self, co3d):
            found_idx = self.find_pt(co3d)
            if found_idx is None:
                if len(self.ls) < self.max_cnt:
                    self.ls.append(co3d.copy())
                    self.cnt += 1
                    self.average()
    
        def reset(self, co3d):
            self.co3d = co3d.copy()
            self.ls = [co3d.copy()]
            self.cnt = 1
    
        def get_co(self):
            return self.co3d.copy()
    
        def print_vals(self):  # debug
            print("self.cnt:", self.cnt)
            print("self.ls:", self.cnt)
            print("self.co3d:", self.co3d)
            for i in range(self.cnt):
                print("  [" + str(i) + "]:", [self.ls[i]])
    
    
    class ReferencePoint:
    
        '''
        Basically this is just a "wrapper" around a 3D coordinate (Vector type)
        to centralize certain Reference Point features and make them easier to
        work with.
        note: if co3d is None, point does not "exist"
        '''
    
    NBurn's avatar
    NBurn committed
        def __init__(self, ptype, colr, co3d=None):
            self.ptype = ptype  # debug?
            self.colr = colr  # color (tuple), for displaying point in 3D view
            self.co3d = co3d  # 3D coordinate (Vector)
    
        # use this method to get co2d because "non-existing" points
        # will lead to a function call like this and throw an error:
        # loc3d_to_reg2d(reg, rv3d, None)
        def get_co2d(self):
            co2d = None
            if self.co3d is not None:
                reg = bpy.context.region
                rv3d = bpy.context.region_data
                co2d = loc3d_to_reg2d(reg, rv3d, self.co3d)
            return co2d
    
        def copy(self):
            return ReferencePoint( self.ptype, self.colr, self.co3d.copy() )
    
        def print_vals(self):  # debug
            print("self.ptype:", self.ptype)
            print("self.colr :", self.colr)
            print("self.co3d :", self.co3d)
    
    
    def init_ref_pts(self):
        self.pts = [
            ReferencePoint("fre", Colr.green),
            ReferencePoint("anc", Colr.red),
            ReferencePoint("piv", Colr.yellow)
        ]
    
    NBurn's avatar
    NBurn committed
    def set_piv(self):
        #if self.pt_cnt == 2:
        if self.pt_cnt == 3:
    
            rpts = tuple(p.co3d for p in self.pts)
            TransDat.piv_norm = geometry.normal(rpts)
    
    NBurn's avatar
    NBurn committed
    
    def set_mouse_highlight(self):
        if self.pt_cnt < 3:
            self.highlight_mouse = True
        else:
            self.highlight_mouse = False
    
    
    def in_ref_pts(self, co3d, skip_idx=None):
        p_idxs = [0, 1, 2][:self.pt_cnt]
        # skip_idx so co3d is not checked against itself
        if skip_idx is not None:
            p_idxs.remove(skip_idx)
        found = False
        for i in p_idxs:
            if vec3s_alm_eq(self.pts[i].co3d, co3d):
                found = True
                self.swap_pt = i  # todo : better solution than this
                break
        return found
    
    
    def add_pt(self, co3d):
        if not in_ref_pts(self, co3d):
            self.pts[self.pt_cnt].co3d = co3d
            self.pt_cnt += 1
            self.menu.change_menu(self.pt_cnt)
            if self.pt_cnt > 1:
    
                update_lock_pts(self, self.pts)
    
    NBurn's avatar
    NBurn committed
            set_mouse_highlight(self)
            ''' Begin Debug
            cnt = self.pt_cnt - 1
            pt_fnd_str = str(self.pts[cnt].co3d)
            pt_fnd_str = pt_fnd_str.replace("<Vector ", "Vector(")
            pt_fnd_str = pt_fnd_str.replace(">", ")")
            print("ref_pt_" + str(cnt) + ' =', pt_fnd_str)
            #print("ref pt added:", self.cnt, "cnt:", self.cnt+1)
            End Debug '''
    
    
    def rem_ref_pt(self, idx):
        # hackery or smart, you decide...
        if idx != self.pt_cnt - 1:
            keep_idx = [0, 1, 2][:self.pt_cnt]
            keep_idx.remove(idx)
            for i in range(len(keep_idx)):
                self.pts[i].co3d = self.pts[keep_idx[i]].co3d.copy()
        self.pt_cnt -= 1
        self.menu.change_menu(self.pt_cnt)
        # set "non-existing" points to None
        for j in range(self.pt_cnt, 3):
            self.pts[j].co3d = None
        if self.pt_cnt > 1:
    
            update_lock_pts(self, self.pts)
    
    NBurn's avatar
    NBurn committed
        else:
    
            TransDat.axis_lock = None
    
    NBurn's avatar
    NBurn committed
        self.highlight_mouse = True
    
    
    def add_select(self):
        if self.pt_cnt < 3:
            if bpy.context.mode == "OBJECT":
                if len(bpy.context.selected_objects) > 0:
                    for obj in bpy.context.selected_objects:
                        add_pt(self, obj.location.copy())
                        if self.pt_cnt > 2:
                            break
            elif bpy.context.mode == "EDIT_MESH":
                m_w = bpy.context.edit_object.matrix_world
                bm = bmesh.from_edit_mesh(bpy.context.edit_object.data)
                if len(bm.select_history) > 0:
                    exit_loop = False  # simplify checking...
                    for sel in bm.select_history:
                        sel_verts = []
                        if type(sel) is bmesh.types.BMVert:
                            sel_verts = [sel]
                        elif type(sel) is bmesh.types.BMEdge:
                            sel_verts = sel.verts
                        elif type(sel) is bmesh.types.BMFace:
                            sel_verts = sel.verts
                        for v in sel_verts:
    
                            v_co3d = m_w @ v.co
    
    NBurn's avatar
    NBurn committed
                            add_pt(self, v_co3d)
                            if self.pt_cnt > 2:
                                exit_loop = True
                                break
                        if exit_loop:
                            break
    
    
    # todo : find way to merge this with add_select ?
    def add_select_multi(self):
        if self.multi_tmp.cnt < self.multi_tmp.max_cnt:
            if bpy.context.mode == "OBJECT":
                if len(bpy.context.selected_objects) > 0:
                    for obj in bpy.context.selected_objects:
                        self.multi_tmp.try_add(obj.location)
                        if self.multi_tmp.cnt == self.multi_tmp.max_cnt:
                            break
            elif bpy.context.mode == "EDIT_MESH":
                m_w = bpy.context.edit_object.matrix_world
                bm = bmesh.from_edit_mesh(bpy.context.edit_object.data)
                if len(bm.select_history) > 0:
                    exit_loop = False  # simplify checking...
                    for sel in bm.select_history:
                        sel_verts = []
                        if type(sel) is bmesh.types.BMVert:
                            sel_verts = [sel]
                        elif type(sel) is bmesh.types.BMEdge:
                            sel_verts = sel.verts
                        elif type(sel) is bmesh.types.BMFace:
                            sel_verts = sel.verts
                        for v in sel_verts:
    
                            v_co3d = m_w @ v.co
    
    NBurn's avatar
    NBurn committed
                            self.multi_tmp.try_add(v_co3d)
                            if self.multi_tmp.cnt == self.multi_tmp.max_cnt:
                                exit_loop = True
                                break
                        if exit_loop:
                            break
            if in_ref_pts(self, self.multi_tmp.get_co(), self.mod_pt):
                self.report({'WARNING'}, 'Points overlap.')
            self.pts[self.mod_pt].co3d = self.multi_tmp.get_co()
    
    
    def swap_ref_pts(self, pt1, pt2):
        temp = self.pts[pt1].co3d.copy()
        self.pts[pt1].co3d = self.pts[pt2].co3d.copy()
        self.pts[pt2].co3d = temp
    
    
    def new_select_multi(self):
    
        '''
        For adding multi point without first needing a reference point
        '''
        # todo : clean up TempPoint so this function isn't needed
        # todo : find way to merge this with add_select_multi
    
    NBurn's avatar
    NBurn committed
        def enable_multi_mode(self):
            if self.grab_pt is not None:
                self.multi_tmp.__init__()
                self.multi_tmp.co3d = Vector()
                self.mod_pt = self.grab_pt
                self.grab_pt = None
            elif self.mod_pt is None:
                self.multi_tmp.__init__()
                self.multi_tmp.co3d = Vector()
                self.mod_pt = self.pt_cnt
                self.pt_cnt += 1
    
        if bpy.context.mode == "OBJECT":
            if len(bpy.context.selected_objects) > 0:
                enable_multi_mode(self)
                for obj in bpy.context.selected_objects:
                    self.multi_tmp.try_add(obj.location)
                    if self.multi_tmp.cnt == self.multi_tmp.max_cnt:
                        break
            else:
                return
        elif bpy.context.mode == "EDIT_MESH":
            m_w = bpy.context.edit_object.matrix_world
            bm = bmesh.from_edit_mesh(bpy.context.edit_object.data)
            if len(bm.select_history) > 0:
                enable_multi_mode(self)
                exit_loop = False  # simplify checking...
                for sel in bm.select_history:
                    sel_verts = []
                    if type(sel) is bmesh.types.BMVert:
                        sel_verts = [sel]
                    elif type(sel) is bmesh.types.BMEdge:
                        sel_verts = sel.verts
                    elif type(sel) is bmesh.types.BMFace:
                        sel_verts = sel.verts
                    for v in sel_verts:
    
                        v_co3d = m_w @ v.co
    
    NBurn's avatar
    NBurn committed
                        self.multi_tmp.try_add(v_co3d)
                        if self.multi_tmp.cnt == self.multi_tmp.max_cnt:
                            exit_loop = True
                            break
                    if exit_loop:
                        break
            else:
                return
    
    
    def exit_multi_mode(self):
        m_co3d = self.multi_tmp.get_co()
        if in_ref_pts(self, m_co3d, self.mod_pt):
            self.report({'ERROR'}, "Point overlapped another and was removed.")
            rem_ref_pt(self, self.mod_pt)
        else:
            self.pts[self.mod_pt].co3d = m_co3d
            if self.pt_cnt > 1:
    
                update_lock_pts(self, self.pts)
    
    NBurn's avatar
    NBurn committed
            set_mouse_highlight(self)
        self.mod_pt = None
        set_help_text(self, "CLICK")
    
    
    def get_axis_line_co(p1, p2, x_max, y_max):
        if None not in (p1, p2):
            x_min, y_min = 0.0, 0.0
            x1, y1 = p1
            x2, y2 = p2
            if flts_alm_eq(x1, x2):
                return Vector((x1, y_min)), Vector((x1, y_max))
            elif flts_alm_eq(y1, y2):
                return Vector((x_min, y1)), Vector((x_max, y1))
            tol = 0.0001
            xb_min, xb_max = x_min - tol, x_max + tol
            yb_min, yb_max = y_min - tol, y_max + tol
            ln_pts = []
            slope = (y2 - y1) / (x2 - x1)
    
            x_bot = ((y_min - y1) / slope) + x1
            if x_bot > xb_min and x_bot < xb_max:
                ln_pts.append( Vector((x_bot, y_min)) )
            x_top = ((y_max - y1) / slope) + x1
            if x_top > xb_min and x_top < xb_max:
                ln_pts.append( Vector((x_top, y_max)) )
                if len(ln_pts) > 1: return ln_pts
            y_lef = (slope * (x_min - x1)) + y1
            if y_lef > yb_min and y_lef < yb_max:
                ln_pts.append( Vector((x_min, y_lef)) )
                if len(ln_pts) > 1: return ln_pts
            y_rgt = (slope * (x_max - x1)) + y1
            if y_rgt > yb_min and y_rgt < yb_max:
                ln_pts.append( Vector((x_max, y_rgt)) )
                if len(ln_pts) > 1: return ln_pts
    
    
    def find_closest_point(loc):
    
        '''
        Returns the closest object origin or vertex to the supplied
        2D location as a 3D Vector.
        Returns None if no coordinates are found within the minimum distance.
        '''
    
    NBurn's avatar
    NBurn committed
        region = bpy.context.region
        rv3d = bpy.context.region_data
        shortest_dist = 40.0  # minimum distance from loc
        closest = None
        for obj in bpy.context.scene.objects:
            o_co2d = loc3d_to_reg2d(region, rv3d, obj.location)
            if o_co2d is None:
                continue
            dist2d = (loc - o_co2d).length
            if dist2d < shortest_dist:
                shortest_dist = dist2d
                closest = obj.location.copy()
            if obj.type == 'MESH':
                if len(obj.data.vertices) > 0:
                    for v in obj.data.vertices:
    
                        v_co3d = obj.matrix_world @ v.co
    
    NBurn's avatar
    NBurn committed
                        v_co2d = loc3d_to_reg2d(region, rv3d, v_co3d)
                        if v_co2d is not None:
                            dist2d = (loc - v_co2d).length
                            if dist2d < shortest_dist:
                                shortest_dist = dist2d
                                closest = v_co3d
        return closest
    
    
    NBurn's avatar
    NBurn committed
    def draw_pt_2d(pt_co, pt_color, pt_size):
        if pt_co is not None:
            bgl.glEnable(bgl.GL_BLEND)
            bgl.glPointSize(pt_size)
            bgl.glColor4f(*pt_color)
            bgl.glBegin(bgl.GL_POINTS)
            bgl.glVertex2f(*pt_co)
            bgl.glEnd()
        return
    
    def draw_pt_2d(pt_co, pt_color, pt_size):
        if pt_co is not None:
            coords = [pt_co]
            bgl.glPointSize(pt_size)
            shader = gpu.shader.from_builtin('2D_UNIFORM_COLOR')
            batch = batch_for_shader(shader, 'POINTS', {"pos": coords})
            shader.bind()
            shader.uniform_float("color", pt_color)
            batch.draw(shader)
    
    NBurn's avatar
    NBurn committed
    def draw_line_2d(pt_co_1, pt_co_2, pt_color):
        if None not in (pt_co_1, pt_co_2):
            bgl.glEnable(bgl.GL_BLEND)
            bgl.glPointSize(15)
            bgl.glColor4f(*pt_color)
            bgl.glBegin(bgl.GL_LINE_STRIP)
            bgl.glVertex2f(*pt_co_1)
            bgl.glVertex2f(*pt_co_2)
            bgl.glEnd()
        return
    
    '''
    
    def draw_line_2d(pt_co_1, pt_co_2, pt_color):
        if None not in (pt_co_1, pt_co_2):
            coords = [pt_co_1, pt_co_2]
            shader = gpu.shader.from_builtin('2D_UNIFORM_COLOR')
            batch = batch_for_shader(shader, 'LINES', {"pos": coords})
            shader.bind()
            shader.uniform_float("color", pt_color)
            batch.draw(shader)
    
    NBurn's avatar
    NBurn committed
    
    
    def closest_to_point(pt, pts):
        smallest_dist = 15.0
        closest, pt_idx = None, None
        for p in range(len(pts)):
            if pts[p] is not None:
                tmp_d = (pt - pts[p]).length
                if tmp_d < smallest_dist:
                    smallest_dist = tmp_d
                    closest = pts[p]
                    pt_idx = p
        return closest, pt_idx
    
    
    def set_arc_pts(ref_pts):
        fre, anc, piv = ref_pts[0].co3d, ref_pts[1].co3d, ref_pts[2].co3d
        arc_pts = []
        ang = (fre - piv).angle(anc - piv)
        deg_ang = degrees(ang)
        if deg_ang > 0.01 and deg_ang < 179.99:
            piv_norm = geometry.normal(fre, piv, anc)
            rot_val = Quaternion(piv_norm, ang)
            rotated = fre - piv
            rotated.rotate(rot_val)
            rotated += piv
            rot_ang = (anc - piv).angle(rotated - piv)
            if not flts_alm_eq(rot_ang, 0.0):
                ang = -ang
            dis_p_f = (piv - fre).length
            dis_p_a = (piv - anc).length
            if dis_p_f < dis_p_a:
                ratio = 0.5
            else:  # dis_p_a < dis_p_f:
                ratio = dis_p_a / dis_p_f * 0.5
            mid_piv_free = piv.lerp(fre, ratio)
            arc_pts = [mid_piv_free]
            steps = abs( int(degrees(ang) // 10) )
            ang_step = ang / steps
            mid_align = mid_piv_free - piv
            for a in range(1, steps):
                rot_val = Quaternion(piv_norm, ang_step * a)
                temp = mid_align.copy()
                temp.rotate(rot_val)
                arc_pts.append(temp + piv)
            # in case steps <= 1
            rot_val = Quaternion(piv_norm, ang)
            temp = mid_align.copy()
            temp.rotate(rot_val)
            arc_pts.append(temp + piv)
    
    
        elif TransDat.axis_lock is not None:
            #if TransDat.axis_lock == 'X':
    
    NBurn's avatar
    NBurn committed
            #    rot_val = Euler((pi*2, 0.0, 0.0), 'XYZ')
    
            if TransDat.axis_lock == 'X':
    
    NBurn's avatar
    NBurn committed
                piv_norm = 1.0, 0.0, 0.0
    
            elif TransDat.axis_lock == 'Y':
    
    NBurn's avatar
    NBurn committed
                piv_norm = 0.0, 1.0, 0.0
    
            elif TransDat.axis_lock == 'Z':
    
    NBurn's avatar
    NBurn committed
                piv_norm = 0.0, 0.0, 1.0
            dis_p_f = (piv - fre).length
            dis_p_a = (piv - anc).length
            if dis_p_f < dis_p_a:
                ratio = 0.5
            else:  # dis_p_a < dis_p_f:
                ratio = dis_p_a / dis_p_f * 0.5
            mid_piv_free = piv.lerp(fre, ratio)
            arc_pts = [mid_piv_free]
            steps = 36
    
            ang_step = pi * 2 / steps
    
    NBurn's avatar
    NBurn committed
            mid_align = mid_piv_free - piv
            for a in range(1, steps+1):
                rot_val = Quaternion(piv_norm, ang_step * a)
                temp = mid_align.copy()
                temp.rotate(rot_val)
                arc_pts.append(temp + piv)
    
    
        TransDat.arc_pts = arc_pts
    
    NBurn's avatar
    NBurn committed
    
    
    def set_lock_pts(ref_pts, pt_cnt):
    
        '''
        Takes a ref_pts (ReferencePoints class) argument and modifies
        its member variable lp_ls (lock pt list). The lp_ls variable is
        assigned a modified list of 3D coordinates (if an axis lock was
        provided), the contents of the ref_pts' rp_ls var (if no axis
        lock was provided), or an empty list (if there wasn't enough
        ref_pts or there was a problem creating the modified list).
        '''
        # todo : move inside ReferencePoints class ?
    
    NBurn's avatar
    NBurn committed
        if pt_cnt < 2:
    
            TransDat.lock_pts = []
        elif TransDat.axis_lock is None:
            TransDat.lock_pts = ref_pts
    
    NBurn's avatar
    NBurn committed
            if pt_cnt == 3:
                set_arc_pts(ref_pts)
    
    
    def get_line_ang_3d(end_a, piv_pt, end_b):
    
        '''
        end_a, piv_pt, and end_b are Vector based 3D coordinates
        coordinates must share a common center "pivot" point (piv_pt)
        '''
    
    NBurn's avatar
    NBurn committed
        algn_a = end_a - piv_pt
        algn_b = end_b - piv_pt
        return algn_a.angle(algn_b)
    
    
    def ang_match3d(end_a, piv_pt, end_b, exp_ang):
    
        '''
        Checks if the 3 Vector coordinate arguments (end_a, piv_pt, end_b)
        will create an angle with a measurement matching the value in the
        argument exp_ang (expected angle measurement).
        '''
    
    NBurn's avatar
    NBurn committed
        ang_meas = get_line_ang_3d(end_a, piv_pt, end_b)
        #print("end_a", end_a)  # debug
        #print("piv_pt", piv_pt)  # debug
        #print("end_b", end_b)  # debug
        #print("exp_ang ", exp_ang)  # debug
        #print("ang_meas ", ang_meas)  # debug
        return flts_alm_eq(ang_meas, exp_ang)
    
    
    
    def create_z_orient(rot_vec):
        x_dir_p = Vector(( 1.0,  0.0,  0.0))
        y_dir_p = Vector(( 0.0,  1.0,  0.0))
        z_dir_p = Vector(( 0.0,  0.0,  1.0))
        if flt_lists_alm_eq(rot_vec, (0.0, 0.0, 0.0)) or \
                flt_lists_alm_eq(rot_vec, z_dir_p):
            return Matrix((x_dir_p, y_dir_p, z_dir_p))  # 3x3 identity
        new_z = rot_vec.copy()  # rot_vec already normalized
    
    NBurn's avatar
    NBurn committed
        new_y = new_z.cross(z_dir_p)
    
        if flt_lists_alm_eq(new_y, (0.0, 0.0, 0.0)):
            new_y = y_dir_p
        new_x = new_y.cross(new_z)
        new_x.normalize()
        new_y.normalize()
        return Matrix(((new_x.x, new_y.x, new_z.x),
                       (new_x.y, new_y.y, new_z.y),
                       (new_x.z, new_y.z, new_z.z)))
    
    
    
    def update_lock_pts(self, ref_pts):
    
        '''
        Updates lock points and changes curr_meas_stor to use measure based on
        lock points instead of ref_pts (for axis constrained transformations).
        '''
    
    NBurn's avatar
    NBurn committed
        global curr_meas_stor
        set_lock_pts(ref_pts, self.pt_cnt)
    
        if TransDat.lock_pts == []:
            if TransDat.axis_lock is not None:
                self.report({'ERROR'}, 'Axis lock \''+ TransDat.axis_lock+
    
    NBurn's avatar
    NBurn committed
                        '\' creates identical points')
    
            TransDat.lock_pts = ref_pts
            TransDat.axis_lock = None
    
    NBurn's avatar
    NBurn committed
    
    
    def axis_key_check(self, new_axis):
    
        '''
        See if key was pressed that would require updating the axis lock info.
        If one was, update the lock points to use new info.
        '''
    
    NBurn's avatar
    NBurn committed
        if self.pt_cnt == 1:
    
            if new_axis != TransDat.axis_lock:
                TransDat.axis_lock = new_axis
    
    NBurn's avatar
    NBurn committed
    
    
    def draw_rot_arc(colr):
        reg = bpy.context.region
        rv3d = bpy.context.region_data
    
        len_arc_pts = len(TransDat.arc_pts)
    
    NBurn's avatar
    NBurn committed
        if len_arc_pts > 1:
    
            last = loc3d_to_reg2d(reg, rv3d, TransDat.arc_pts[0])
    
    NBurn's avatar
    NBurn committed
            for p in range(1, len_arc_pts):
    
                p2d = loc3d_to_reg2d(reg, rv3d, TransDat.arc_pts[p])
    
    NBurn's avatar
    NBurn committed
                draw_line_2d(last, p2d, Colr.white)
                last = p2d
    
    
    def set_help_text(self, mode):
    
        '''Called when add-on mode changes and every time point is added or removed.'''
    
    NBurn's avatar
    NBurn committed
        text = ""
        if mode == "CLICK":
            if self.pt_cnt == 0:
                text = "ESC/LMB+RMB - exits add-on, LMB - add ref point"
            elif self.pt_cnt == 1:
                text = "ESC/LMB+RMB - exits add-on, LMB - add/remove ref points, X/Y/Z - set axis lock, C - clear axis lock, G - grab point, SHIFT+LMB enter mid point mode"
            elif self.pt_cnt == 2:
                text = "ESC/LMB+RMB - exits add-on, LMB - add/remove ref points, G - grab point, SHIFT+LMB enter mid point mode"
            else:  # self.pt_cnt == 3
                text = "ESC/LMB+RMB - exits add-on, LMB - remove ref points, G - grab point, SHIFT+LMB enter mid point mode"
        elif mode == "MULTI":
            text = "ESC/LMB+RMB - exits add-on, SHIFT+LMB exit mid point mode, LMB - add/remove point"
        elif mode == "GRAB":
            text = "ESC/LMB+RMB - exits add-on, G - cancel grab, LMB - place/swap ref points"
        elif mode == "POPUP":
            text = "ESC/LMB+RMB - exits add-on, LMB/RMB (outside pop-up) - cancel pop-up input"
    
        bpy.context.area.header_text_set(text)