Skip to content
Snippets Groups Projects
xedit_set_meas.py 77 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
    popup_meas_backup = 0.0
    curr_meas_stor = 0.0
    new_meas_stor = None
    popup_active = False
    prev_popup_inputs = []
    prev_popup_inp_strings = []
    
    #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
    
    
    
    # Transformation Data
    # values stored here get used for translation, scale, and rotation
    class TransDat:
    
    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
    # 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.
    def editmode_refresh():
        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)
    
    
    # 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
    
    
    # assume both float lists are same size?
    
    def flt_lists_alm_eq(ls_a, ls_b, tol=0.001):
    
    NBurn's avatar
    NBurn committed
        for i in range(len(ls_a)):
    
            if not (ls_a[i] > (ls_b[i] - tol) and ls_a[i] < (ls_b[i] + tol)):
    
    NBurn's avatar
    NBurn committed
                return False
        return True
    
    
    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, None]  # no menu for 0 or 1
            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.active = False
    
    
            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
            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):
                    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
    
    
    def test_reset_prev_popup_inputs():
        global prev_popup_inputs
        prev_popup_inputs = []
    
    
    def push_temp_meas():
        global prev_popup_inputs, popup_meas_backup
        #print("popup_meas_backup:", popup_meas_backup)  # debug
        max_len = 10
        if popup_meas_backup not in prev_popup_inputs:
            if len(prev_popup_inputs) == max_len:
                prev_popup_inputs.pop()
            prev_popup_inputs.insert(0, popup_meas_backup)
        else:
            if prev_popup_inputs.index(popup_meas_backup) != 0:
                prev_popup_inputs.remove(popup_meas_backup)
                prev_popup_inputs.insert(0, popup_meas_backup)
    
    
    def make_popup_enums(self, context):
        global prev_popup_inputs, prev_popup_inp_strings
    
        prev_popup_inp_strings[:] = [('-', '--', '')]  # reset data
        for i, val in enumerate(prev_popup_inputs):  # gen enum vals
    
    NBurn's avatar
    NBurn committed
            prev_popup_inp_strings.append(( str(i), str(val), '' ))
        return prev_popup_inp_strings
    
    
    
    class XEDIT_OT_store_meas_btn(bpy.types.Operator):
    
    NBurn's avatar
    NBurn committed
        bl_idname = "object.store_meas_inp_op"
    
        bl_label = "Exact Edit Store Measure Button"
    
    NBurn's avatar
    NBurn committed
        bl_description = "Add current measure to stored measures"
        bl_options = {'INTERNAL'}
    
        def invoke(self, context, event):
            #print("StoreMeasBtn: called invoke")
            push_temp_meas()
            return {'FINISHED'}
    
    
    # == pop-up dialog code ==
    # todo: update with newer menu code if it can ever be made to work
    
    class XEDIT_OT_meas_inp_dlg(bpy.types.Operator):
    
    NBurn's avatar
    NBurn committed
        bl_idname = "object.ms_input_dialog_op"
    
        bl_label = "Exact Edit Measure Input Dialog"
    
    NBurn's avatar
    NBurn committed
        bl_options = {'INTERNAL'}
    
    
        float_new_meas: bpy.props.FloatProperty(name="Measurement")
        prev_meas: bpy.props.EnumProperty(
    
    NBurn's avatar
    NBurn committed
                        items=make_popup_enums,
                        name="Last measure",
                        description="Last 5 measurements entered")
    
        def execute(self, context):
            global popup_active, new_meas_stor
            new_meas_stor = self.float_new_meas
            popup_active = False
            push_temp_meas()
            return {'FINISHED'}
    
        def invoke(self, context, event):
            global curr_meas_stor
            self.float_new_meas = curr_meas_stor
            return context.window_manager.invoke_props_dialog(self)
    
        def cancel(self, context):
            global popup_active
            #print("Cancelled Pop-Up")  # debug
            popup_active = False
    
        def check(self, context):
            return True
    
        def draw(self, context):
            global popup_meas_backup
            popup_meas_backup = self.float_new_meas
            # below will always evaluate False unless check method returns True
            # todo : move this to check() method ?
            if self.prev_meas != '-':
                global prev_popup_inputs
                int_prev_meas = int(self.prev_meas)
                self.float_new_meas = float(prev_popup_inputs[int_prev_meas])
                self.prev_meas = '-'
    
    
            row = self.layout.row(align=True)
    
    NBurn's avatar
    NBurn committed
            # split row into 3 cells: 1st 1/3, 2nd 75% of 2/3, 3rd 25% of 2/3
            split = row.split(align=False)
            split.label(text="Measurement")
    
            split = row.split(factor=0.75, align=False)
    
    NBurn's avatar
    NBurn committed
            split.prop(self, 'float_new_meas', text="")
            split.operator("object.store_meas_inp_op", text="Store")
    
            row = self.layout.row(align=True)
    
    NBurn's avatar
    NBurn committed
            row.prop(self, 'prev_meas')
    
    
    # === 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)
    
                blf.size(font_id, self.txt_sz, self.dpi)
                blf.color(font_id, *self.txt_colr)
    
    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]])
    
    
    # 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"
    class ReferencePoint:
        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_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:
                updatelock_pts(self, self.pts)
            set_mouse_highlight(self)
            set_meas_btn(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:
            updatelock_pts(self, self.pts)
        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 set_meas_btn(self):
    
        lock_pts = TransDat.lock_pts
    
    NBurn's avatar
    NBurn committed
        if self.pt_cnt == 2:
            global curr_meas_stor
            curr_meas_stor = (lock_pts[0].co3d - lock_pts[1].co3d).length
            self.meas_btn.set_text(format(curr_meas_stor, '.2f'))
        elif self.pt_cnt == 3:
            algn_co1 = lock_pts[0].co3d - lock_pts[2].co3d
            algn_co3 = lock_pts[1].co3d - lock_pts[2].co3d
            curr_meas_stor = degrees( algn_co1.angle(algn_co3) )
            self.meas_btn.set_text(format(curr_meas_stor, '.2f'))
            return
    
    
    # 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
    def new_select_multi(self):
        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:
                updatelock_pts(self, self.pts)
            set_mouse_highlight(self)
        self.mod_pt = None
        set_meas_btn(self)
        set_help_text(self, "CLICK")
    
    
    # Returns the closest object origin or vertex to the supplied 2D location
    # as 3D Vector.
    # Returns None if no found coordinate closer than minimum distance.
    def find_closest_point(loc):
        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
    
    
    # Can a transformation be performed? Called after measure button is clicked
    # to let user know if valid options are set before enabling pop-up to get
    # user input.
    # todo, move transf_type assignment to "point add" part of code?
    def can_transf(self):
        global curr_meas_stor
        success = False
        if self.pt_cnt == 2:
            mode = self.menu.get_mode()
            if mode == "Move":
                self.transf_type = MOVE
                success = True
            elif mode == "Scale":
                self.transf_type = SCALE
                success = True
    
        elif self.pt_cnt == 3:
            self.transf_type = ROTATE
    
            if TransDat.axis_lock is not None:
    
    NBurn's avatar
    NBurn committed
                success = True
            # if not flat angle and no axis lock set, begin preparations for
            # arbitrary axis / spherical rotation
            elif not flts_alm_eq(curr_meas_stor, 0.0) and \
            not flts_alm_eq(curr_meas_stor, 180.0):
    
                rpts = tuple(p.co3d for p in self.pts)
                TransDat.piv_norm = geometry.normal(rpts)
    
    NBurn's avatar
    NBurn committed
                success = True
            else:
                # would need complex angle processing workaround to get
                # spherical rotations working with flat angles. todo item?
                # blocking execution for now.
                self.report({'INFO'}, "Need axis lock for 0 and 180 degree angles.")
        return success
    
    
    # For making sure rise over run doesn't get flipped.
    def slope_check(pt1, pt2):
        cmp_ls = []
        for i in range(len(pt1)):
            cmp_ls.append(flts_alm_eq(pt1[i], pt2[i]) or pt1[i] > pt2[i])
        return cmp_ls
    
    
    # Finds 3D location that shares same slope of line connecting Anchor and
    # Free or that is on axis line going through Anchor.
    def get_new_3d_co(self, old_dis, new_dis):
        pt_anc, pt_fr = self.pts[1].co3d, self.pts[0].co3d
    
        if TransDat.axis_lock is None:
    
    NBurn's avatar
    NBurn committed
            if new_dis == 0:
                return pt_anc
            orig_slope = slope_check(pt_anc, pt_fr)
            scale = new_dis / old_dis
            pt_pos = pt_anc.lerp(pt_fr,  scale)
            pt_neg = pt_anc.lerp(pt_fr, -scale)
            pt_pos_slp = slope_check(pt_anc, pt_pos)
            pt_neg_slp = slope_check(pt_anc, pt_neg)
            # note: slope_check returns 3 bool values
            if orig_slope == pt_pos_slp:
                if new_dis > 0:
                    return pt_pos
                else:
                    # for negative distances
                    return pt_neg
            elif orig_slope == pt_neg_slp:
                if new_dis > 0:
                    return pt_neg
                else:
                    return pt_pos
            else:  # neither slope matches
                self.report({'ERROR'}, 'Slope mismatch. Cannot calculate new point.')
                return None
    
    
        elif TransDat.axis_lock == 'X':
    
    NBurn's avatar
    NBurn committed
            if pt_fr[0] > pt_anc[0]:
                return Vector([ pt_anc[0] + new_dis, pt_fr[1], pt_fr[2] ])
            else:
                return Vector([ pt_anc[0] - new_dis, pt_fr[1], pt_fr[2] ])
    
        elif TransDat.axis_lock == 'Y':
    
    NBurn's avatar
    NBurn committed
            if pt_fr[1] > pt_anc[1]:
                return Vector([ pt_fr[0], pt_anc[1] + new_dis, pt_fr[2] ])
            else:
                return Vector([ pt_fr[0], pt_anc[1] - new_dis, pt_fr[2] ])
    
        elif TransDat.axis_lock == 'Z':
    
    NBurn's avatar
    NBurn committed
            if pt_fr[2] > pt_anc[2]:
                return Vector([ pt_fr[0], pt_fr[1], pt_anc[2] + new_dis ])
            else:
                return Vector([ pt_fr[0], pt_fr[1], pt_anc[2] - new_dis ])
        else:  # neither slope matches
            self.report({'ERROR'}, "Slope mismatch. Can't calculate new point.")
            return None
    
    
    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)