Update for API change: scene.cursor_location -> scene.cursor.location
[blender-addons-contrib.git] / exact_edit / xedit_set_meas.py
1 '''
2 BEGIN GPL LICENSE BLOCK
3
4 This program is free software; you can redistribute it and/or
5 modify it under the terms of the GNU General Public License
6 as published by the Free Software Foundation; either version 2
7 of the License, or (at your option) any later version.
8
9 This program is distributed in the hope that it will be useful,
10 but WITHOUT ANY WARRANTY; without even the implied warranty of
11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 GNU General Public License for more details.
13
14 You should have received a copy of the GNU General Public License
15 along with this program; if not, write to the Free Software Foundation,
16 Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
18 END GPL LICENSE BLOCK
19 '''
20
21 from copy import deepcopy
22 from math import degrees, radians, pi
23
24 import bpy
25 import bmesh
26 import bgl
27 import blf
28 from mathutils import geometry, Euler, Quaternion, Vector
29 from bpy_extras import view3d_utils
30 from bpy_extras.view3d_utils import location_3d_to_region_2d as loc3d_to_reg2d
31 from bpy_extras.view3d_utils import region_2d_to_vector_3d as reg2d_to_vec3d
32 from bpy_extras.view3d_utils import region_2d_to_location_3d as reg2d_to_loc3d
33 from bpy_extras.view3d_utils import region_2d_to_origin_3d as reg2d_to_org3d
34
35 # "Constant" values
36 (
37     X,
38     Y,
39     Z,
40
41     CLICK_CHECK,
42     WAIT_FOR_POPUP,
43     GET_0_OR_180,
44     DO_TRANSFORM,
45
46     MOVE,
47     SCALE,
48     ROTATE,
49 ) = range(10)
50
51 # globals
52 popup_meas_backup = 0.0
53 curr_meas_stor = 0.0
54 new_meas_stor = None
55 popup_active = False
56 prev_popup_inputs = []
57 prev_popup_inp_strings = []
58
59 #print("Loaded add-on.\n")  # debug
60
61
62 class Colr:
63     red    = 1.0, 0.0, 0.0, 0.6
64     green  = 0.0, 1.0, 0.0, 0.6
65     blue   = 0.0, 0.0, 1.0, 0.6
66     white  = 1.0, 1.0, 1.0, 1.0
67     grey   = 1.0, 1.0, 1.0, 0.4
68     black  = 0.0, 0.0, 0.0, 1.0
69     yellow = 1.0, 1.0, 0.0, 0.6
70     brown  = 0.15, 0.15, 0.15, 0.20
71
72
73 class RotDat:
74     placeholder = True
75
76
77 # Refreshes mesh drawing in 3D view and updates mesh coordinate
78 # data so ref_pts are drawn at correct locations.
79 # Using editmode_toggle to do this seems hackish, but editmode_toggle seems
80 # to be the only thing that updates both drawing and coordinate info.
81 def editmode_refresh():
82     if bpy.context.mode == "EDIT_MESH":
83         bpy.ops.object.editmode_toggle()
84         bpy.ops.object.editmode_toggle()
85
86
87 def backup_blender_settings():
88     backup = [
89         deepcopy(bpy.context.tool_settings.use_snap),
90         deepcopy(bpy.context.tool_settings.snap_element),
91         deepcopy(bpy.context.tool_settings.snap_target),
92         deepcopy(bpy.context.space_data.pivot_point),
93         deepcopy(bpy.context.space_data.transform_orientation),
94         deepcopy(bpy.context.space_data.show_manipulator),
95         deepcopy(bpy.context.scene.cursor.location)]
96     return backup
97
98
99 def init_blender_settings():
100     bpy.context.tool_settings.use_snap = False
101     bpy.context.tool_settings.snap_element = 'VERTEX'
102     bpy.context.tool_settings.snap_target = 'CLOSEST'
103     bpy.context.space_data.pivot_point = 'ACTIVE_ELEMENT'
104     bpy.context.space_data.transform_orientation = 'GLOBAL'
105     bpy.context.space_data.show_manipulator = False
106     return
107
108
109 def restore_blender_settings(backup):
110     bpy.context.tool_settings.use_snap = deepcopy(backup[0])
111     bpy.context.tool_settings.snap_element = deepcopy(backup[1])
112     bpy.context.tool_settings.snap_target = deepcopy(backup[2])
113     bpy.context.space_data.pivot_point = deepcopy(backup[3])
114     bpy.context.space_data.transform_orientation = deepcopy(backup[4])
115     bpy.context.space_data.show_manipulator = deepcopy(backup[5])
116     bpy.context.scene.cursor.location = deepcopy(backup[6])
117     return
118
119
120 def flts_alm_eq(flt_a, flt_b):
121     tol = 0.0001
122     return flt_a > (flt_b - tol) and flt_a < (flt_b + tol)
123
124
125 # todo : replace with flt_lists_alm_eq?
126 def vec3s_alm_eq(vec_a, vec_b):
127     X, Y, Z = 0, 1, 2
128     if flts_alm_eq(vec_a[X], vec_b[X]):
129         if flts_alm_eq(vec_a[Y], vec_b[Y]):
130             if flts_alm_eq(vec_a[Z], vec_b[Z]):
131                 return True
132     return False
133
134
135 # assume both float lists are same size?
136 def flt_lists_alm_eq(ls_a, ls_b):
137     for i in range(len(ls_a)):
138         if not flts_alm_eq(ls_a[i], ls_b[i]):
139             return False
140     return True
141
142
143 class MenuStore:
144     def __init__(self):
145         self.cnt = 0
146         self.active = 0  # unused ?
147         # todo : replace above with self.current ?
148         self.txtcolrs = []
149         self.tcoords = []
150         self.texts = []
151         self.arrows = []  # arrow coordinates
152
153
154 class MenuHandler:
155     def __init__(self, title, tsize, act_colr, dis_colr, toolwid, reg):
156         self.dpi = bpy.context.preferences.system.dpi
157         self.title = title
158         # todo : better solution than None "magic numbers"
159         self.menus = [None, None]  # no menu for 0 or 1
160         self.menu_cnt = len(self.menus)
161         self.current = 0  # current active menu
162         self.tsize = tsize  # text size
163         self.act_colr = act_colr
164         self.dis_colr = dis_colr  # disabled color
165         self.reg = reg  # region
166         self.active = False
167
168         view_offset = 36, 45  # box left top start
169         self.box_y_pad = 8  # vertical space between boxes
170
171         fontid = 0
172         blf.size(fontid, tsize, self.dpi)
173         lcase_wid, lcase_hgt = blf.dimensions(fontid, "n")
174         ucase_wid, ucase_hgt = blf.dimensions(fontid, "N")
175         bot_space = blf.dimensions(fontid, "gp")[1] - lcase_hgt
176         self.full_hgt = blf.dimensions(fontid, "NTgp")[1]
177
178         arr_wid, arr_hgt = 12, 16
179         arrow_base = (0, 0), (0, arr_hgt), (arr_wid, arr_hgt/2)
180         aw_adj, ah_adj = arr_wid * 1.5, (arr_hgt - ucase_hgt) / 2
181         self.arrow_pts = []
182         for a in arrow_base:
183             self.arrow_pts.append((a[0] - aw_adj, a[1] - ah_adj))
184
185         self.blef = view_offset[0] + toolwid  # box left start
186         self.titlco = self.blef // 2, self.reg.height - view_offset[1]
187         self.btop = self.titlco[1] - (self.full_hgt // 1.5)
188         self.txt_y_pad = bot_space * 2
189
190     def add_menu(self, strings):
191         self.menus.append(MenuStore())
192         new = self.menus[-1]
193         btop = self.btop
194         tlef = self.blef  # text left
195         new.cnt = len(strings)
196         for i in range(new.cnt):
197             new.txtcolrs.append(self.dis_colr)
198             new.texts.append(strings[i])
199             bbot = btop - self.full_hgt
200             new.tcoords.append((tlef, bbot))
201             btop = bbot - self.box_y_pad
202             new.arrows.append((
203                 (self.arrow_pts[0][0] + tlef, self.arrow_pts[0][1] + bbot),
204                 (self.arrow_pts[1][0] + tlef, self.arrow_pts[1][1] + bbot),
205                 (self.arrow_pts[2][0] + tlef, self.arrow_pts[2][1] + bbot)))
206         new.txtcolrs[new.active] = self.act_colr
207         self.menu_cnt += 1
208
209     def update_active(self, change):
210         menu = self.menus[self.current]
211         if menu is None:
212             return
213         menu.txtcolrs[menu.active] = self.dis_colr
214         menu.active = (menu.active + change) % menu.cnt
215         menu.txtcolrs[menu.active] = self.act_colr
216
217     def change_menu(self, new):
218         self.current = new
219
220     def get_mode(self):
221         menu = self.menus[self.current]
222         return menu.texts[menu.active]
223
224     #def rebuild_menus(self)  # add in case blender window size changes?
225     #    return
226
227     def draw(self, menu_visible):
228         menu = self.menus[self.current]
229         # prepare to draw text
230         font_id = 0
231         blf.size(font_id, self.tsize, self.dpi)
232         # draw title
233         bgl.glColor4f(*self.dis_colr)
234         blf.position(font_id, self.titlco[0], self.titlco[1], 0)
235         blf.draw(font_id, self.title)
236         # draw menu
237         if menu_visible and menu is not None:
238             for i in range(menu.cnt):
239                 bgl.glColor4f(*menu.txtcolrs[i])
240                 blf.position(font_id, menu.tcoords[i][0], menu.tcoords[i][1], 0)
241                 blf.draw(font_id, menu.texts[i])
242
243             # draw arrow
244             bgl.glEnable(bgl.GL_BLEND)
245             bgl.glColor4f(*self.act_colr)
246             bgl.glBegin(bgl.GL_LINE_LOOP)
247             for p in menu.arrows[menu.active]:
248                 bgl.glVertex2f(*p)
249             bgl.glEnd()
250
251
252 def test_reset_prev_popup_inputs():
253     global prev_popup_inputs
254     prev_popup_inputs = []
255
256
257 def push_temp_meas():
258     global prev_popup_inputs, popup_meas_backup
259     #print("popup_meas_backup:", popup_meas_backup)  # debug
260     max_len = 10
261     if popup_meas_backup not in prev_popup_inputs:
262         if len(prev_popup_inputs) == max_len:
263             prev_popup_inputs.pop()
264         prev_popup_inputs.insert(0, popup_meas_backup)
265     else:
266         if prev_popup_inputs.index(popup_meas_backup) != 0:
267             prev_popup_inputs.remove(popup_meas_backup)
268             prev_popup_inputs.insert(0, popup_meas_backup)
269
270
271 def make_popup_enums(self, context):
272     global prev_popup_inputs, prev_popup_inp_strings
273     prev_popup_inp_strings[:] = [('-', '--', '')]
274     for i, val in enumerate(prev_popup_inputs):
275         prev_popup_inp_strings.append(( str(i), str(val), '' ))
276     return prev_popup_inp_strings
277
278
279 class XEditStoreMeasBtn(bpy.types.Operator):
280     bl_idname = "object.store_meas_inp_op"
281     bl_label = "XEdit Store Measure Button"
282     bl_description = "Add current measure to stored measures"
283     bl_options = {'INTERNAL'}
284
285     def invoke(self, context, event):
286         #print("StoreMeasBtn: called invoke")
287         push_temp_meas()
288         return {'FINISHED'}
289
290
291 # == pop-up dialog code ==
292 # todo: update with newer menu code if it can ever be made to work
293 class XEditMeasureInputPanel(bpy.types.Operator):
294     bl_idname = "object.ms_input_dialog_op"
295     bl_label = "XEdit Measure Input Panel"
296     bl_options = {'INTERNAL'}
297
298     float_new_meas: bpy.props.FloatProperty(name="Measurement")
299     prev_meas: bpy.props.EnumProperty(
300                     items=make_popup_enums,
301                     name="Last measure",
302                     description="Last 5 measurements entered")
303
304     def execute(self, context):
305         global popup_active, new_meas_stor
306         new_meas_stor = self.float_new_meas
307         popup_active = False
308         push_temp_meas()
309         return {'FINISHED'}
310
311     def invoke(self, context, event):
312         global curr_meas_stor
313         self.float_new_meas = curr_meas_stor
314         return context.window_manager.invoke_props_dialog(self)
315
316     def cancel(self, context):
317         global popup_active
318         #print("Cancelled Pop-Up")  # debug
319         popup_active = False
320
321     def check(self, context):
322         return True
323
324     def draw(self, context):
325         global popup_meas_backup
326         popup_meas_backup = self.float_new_meas
327         # below will always evaluate False unless check method returns True
328         # todo : move this to check() method ?
329         if self.prev_meas != '-':
330             global prev_popup_inputs
331             int_prev_meas = int(self.prev_meas)
332             self.float_new_meas = float(prev_popup_inputs[int_prev_meas])
333             self.prev_meas = '-'
334
335         row = self.layout.row(align=True)
336         # split row into 3 cells: 1st 1/3, 2nd 75% of 2/3, 3rd 25% of 2/3
337         split = row.split(align=False)
338         split.label(text="Measurement")
339         split = row.split(percentage=0.75, align=False)
340         split.prop(self, 'float_new_meas', text="")
341         split.operator("object.store_meas_inp_op", text="Store")
342         row = self.layout.row(align=True)
343         row.prop(self, 'prev_meas')
344
345
346 # === 3D View mouse location and button code ===
347 class ViewButton():
348     def __init__(self, colr_on, colr_off, txt_sz, txt_colr, offs=(0, 0)):
349         self.dpi = bpy.context.preferences.system.dpi
350         self.is_drawn = False
351         self.ms_over = False  # mouse over button
352         self.wid = 0
353         self.coords = None
354         #self.co_outside_btn = None
355         self.co2d = None
356         self.colr_off = colr_off  # colr when mouse not over button
357         self.colr_on = colr_on  # colr when mouse over button
358         self.txt = ""
359         self.txt_sz = txt_sz
360         self.txt_colr = txt_colr
361         self.txt_co = None
362         self.offset = Vector(offs)
363
364         # Set button height and text offsets (to determine where text would
365         # be placed within button). Done in __init__ as this will not change
366         # during program execution and prevents having to recalculate these
367         # values every time text is changed.
368         font_id = 0
369         blf.size(font_id, self.txt_sz, self.dpi)
370         samp_txt_max = "Tgp"  # text with highest and lowest pixel values
371         x, max_y =  blf.dimensions(font_id, samp_txt_max)
372         y = blf.dimensions(font_id, "T")[1]  # T = sample text
373         y_diff = (max_y - y)
374
375         self.hgt = int(max_y + (y_diff * 2))
376         self.txt_x_offs = int(x / (len(samp_txt_max) * 2) )
377         self.txt_y_offs = int(( self.hgt - y) / 2) + 1
378         # added 1 to txt_y_offs to compensate for possible int rounding
379
380     # replace text string and update button width
381     def set_text(self, txt):
382         font_id = 0
383         self.txt = txt
384         blf.size(font_id, self.txt_sz, self.dpi)
385         w = blf.dimensions(font_id, txt)[0]  # get text width
386         self.wid = w + (self.txt_x_offs * 2)
387         return
388
389     def set_btn_coor(self, co2d):
390         #offs_2d = Vector((-self.wid / 2, 25))
391         offs_2d = Vector((-self.wid / 2, 0))
392         new2d = co2d + offs_2d
393         
394         # co_bl == coordinate bottom left, co_tr == coordinate top right
395         co_bl = new2d[0], new2d[1]
396         co_tl = new2d[0], new2d[1] + self.hgt
397         co_tr = new2d[0] + self.wid, new2d[1] + self.hgt
398         co_br = new2d[0] + self.wid, new2d[1]
399         self.coords = co_bl, co_tl, co_tr, co_br
400         self.txt_co = new2d[0] + self.txt_x_offs, new2d[1] + self.txt_y_offs
401         self.ms_chk = co_bl[0], co_tr[0], co_bl[1], co_tr[1]
402
403     def pt_inside_btn2(self, mouse_co):
404         mx, my = mouse_co[0], mouse_co[1]
405         if mx < self.ms_chk[0] or mx > self.ms_chk[1]:
406             return False
407         if my < self.ms_chk[2] or my > self.ms_chk[3]:
408             return False
409         return True
410     
411     def draw_btn(self, btn_loc, mouse_co, highlight_mouse=False):
412         if btn_loc is not None:
413             offs_loc = btn_loc + self.offset
414             font_id = 0
415             colr = self.colr_off
416             self.set_btn_coor(offs_loc)
417             if self.pt_inside_btn2(mouse_co):
418                 colr = self.colr_on
419                 self.ms_over = True
420             else:
421                 self.ms_over = False
422             # draw button box
423             bgl.glColor4f(*colr)
424             bgl.glBegin(bgl.GL_LINE_STRIP)
425             for coord in self.coords:
426                 bgl.glVertex2f(coord[0], coord[1])
427             bgl.glVertex2f(self.coords[0][0], self.coords[0][1])
428             bgl.glEnd()
429             # draw outline around button box
430             if highlight_mouse and self.ms_over:
431                 bgl.glColor4f(*self.colr_off)
432                 HO = 4  # highlight_mouse offset
433                 offs = (-HO, -HO), (-HO, HO), (HO, HO), (HO, -HO)
434                 bgl.glBegin(bgl.GL_LINE_STRIP)
435                 for i, coord in enumerate(self.coords):
436                     bgl.glVertex2f(coord[0] + offs[i][0], coord[1] + offs[i][1])
437                 bgl.glVertex2f(self.coords[0][0] + offs[0][0], self.coords[0][1] + offs[0][1])
438                 bgl.glEnd()
439             # draw button text
440             bgl.glColor4f(*self.txt_colr)
441             blf.size(font_id, self.txt_sz, self.dpi)
442             blf.position(font_id, self.txt_co[0], self.txt_co[1], 0)
443             blf.draw(font_id, self.txt)
444         else:
445             self.ms_over = False
446
447
448 # Used for mod_pt mode
449 class TempPoint():
450     def __init__(self):
451         self.ls = []  # point list
452         self.cnt = 0
453         self.co3d = None
454         self.max_cnt = 50
455
456     def average(self):
457         vsum = Vector()
458         for p in self.ls:
459             vsum += p
460         self.co3d = vsum / self.cnt
461
462     def find_pt(self, co3d):
463         found_idx = None
464         for i in range(self.cnt):
465             if self.ls[i] == co3d:
466                 found_idx = i
467                 break
468         return found_idx
469
470     def rem_pt(self, idx):
471         self.ls.pop(idx)
472         self.cnt -= 1
473         if self.cnt > 0:
474             self.average()
475         else:
476             self.co3d = None
477
478     def try_add(self, co3d):
479         found_idx = self.find_pt(co3d)
480         if found_idx is None:
481             if len(self.ls) < self.max_cnt:
482                 self.ls.append(co3d.copy())
483                 self.cnt += 1
484                 self.average()
485
486     def reset(self, co3d):
487         self.co3d = co3d.copy()
488         self.ls = [co3d.copy()]
489         self.cnt = 1
490
491     def get_co(self):
492         return self.co3d.copy()
493
494     def print_vals(self):  # debug
495         print("self.cnt:", self.cnt)
496         print("self.ls:", self.cnt)
497         print("self.co3d:", self.co3d)
498         for i in range(self.cnt):
499             print("  [" + str(i) + "]:", [self.ls[i]])
500
501
502 # Basically this is just a "wrapper" around a 3D coordinate (Vector type)
503 # to centralize certain Reference Point features and make them easier to
504 # work with.
505 # note: if co3d is None, point does not "exist"
506 class ReferencePoint:
507     def __init__(self, ptype, colr, co3d=None):
508         self.ptype = ptype  # debug?
509         self.colr = colr  # color (tuple), for displaying point in 3D view
510         self.co3d = co3d  # 3D coordinate (Vector)
511
512     # use this method to get co2d because "non-existing" points
513     # will lead to a function call like this and throw an error:
514     # loc3d_to_reg2d(reg, rv3d, None)
515     def get_co2d(self):
516         co2d = None
517         if self.co3d is not None:
518             reg = bpy.context.region
519             rv3d = bpy.context.region_data
520             co2d = loc3d_to_reg2d(reg, rv3d, self.co3d)
521         return co2d
522
523     def copy(self):
524         return ReferencePoint( self.ptype, self.colr, self.co3d.copy() )
525
526     def print_vals(self):  # debug
527         print("self.ptype:", self.ptype)
528         print("self.colr :", self.colr)
529         print("self.co3d :", self.co3d)
530
531
532 def init_ref_pts(self):
533     self.pts = [
534         ReferencePoint("fre", Colr.green),
535         ReferencePoint("anc", Colr.red),
536         ReferencePoint("piv", Colr.yellow)
537     ]
538     # todo : move this part of initialization elsewhere?
539     RotDat.piv_norm = None
540     RotDat.new_ang_r = None
541     RotDat.ang_diff_r = None
542     RotDat.axis_lock = None
543     RotDat.lock_pts = None
544     RotDat.rot_pt_pos = None
545     RotDat.rot_pt_neg = None
546     RotDat.arc_pts = None
547
548
549 def set_mouse_highlight(self):
550     if self.pt_cnt < 3:
551         self.highlight_mouse = True
552     else:
553         self.highlight_mouse = False
554
555
556 def in_ref_pts(self, co3d, skip_idx=None):
557     p_idxs = [0, 1, 2][:self.pt_cnt]
558     # skip_idx so co3d is not checked against itself
559     if skip_idx is not None:
560         p_idxs.remove(skip_idx)
561     found = False
562     for i in p_idxs:
563         if vec3s_alm_eq(self.pts[i].co3d, co3d):
564             found = True
565             self.swap_pt = i  # todo : better solution than this
566             break
567     return found
568
569
570 def add_pt(self, co3d):
571     if not in_ref_pts(self, co3d):
572         self.pts[self.pt_cnt].co3d = co3d
573         self.pt_cnt += 1
574         self.menu.change_menu(self.pt_cnt)
575         if self.pt_cnt > 1:
576             updatelock_pts(self, self.pts)
577         set_mouse_highlight(self)
578         set_meas_btn(self)
579         ''' Begin Debug
580         cnt = self.pt_cnt - 1
581         pt_fnd_str = str(self.pts[cnt].co3d)
582         pt_fnd_str = pt_fnd_str.replace("<Vector ", "Vector(")
583         pt_fnd_str = pt_fnd_str.replace(">", ")")
584         print("ref_pt_" + str(cnt) + ' =', pt_fnd_str)
585         #print("ref pt added:", self.cnt, "cnt:", self.cnt+1)
586         End Debug '''
587
588
589 def rem_ref_pt(self, idx):
590     # hackery or smart, you decide...
591     if idx != self.pt_cnt - 1:
592         keep_idx = [0, 1, 2][:self.pt_cnt]
593         keep_idx.remove(idx)
594         for i in range(len(keep_idx)):
595             self.pts[i].co3d = self.pts[keep_idx[i]].co3d.copy()
596     self.pt_cnt -= 1
597     self.menu.change_menu(self.pt_cnt)
598     # set "non-existing" points to None
599     for j in range(self.pt_cnt, 3):
600         self.pts[j].co3d = None
601     if self.pt_cnt > 1:
602         updatelock_pts(self, self.pts)
603     else:
604         RotDat.axis_lock = None
605     self.highlight_mouse = True
606
607
608 def add_select(self):
609     if self.pt_cnt < 3:
610         if bpy.context.mode == "OBJECT":
611             if len(bpy.context.selected_objects) > 0:
612                 for obj in bpy.context.selected_objects:
613                     add_pt(self, obj.location.copy())
614                     if self.pt_cnt > 2:
615                         break
616         elif bpy.context.mode == "EDIT_MESH":
617             m_w = bpy.context.edit_object.matrix_world
618             bm = bmesh.from_edit_mesh(bpy.context.edit_object.data)
619             if len(bm.select_history) > 0:
620                 exit_loop = False  # simplify checking...
621                 for sel in bm.select_history:
622                     sel_verts = []
623                     if type(sel) is bmesh.types.BMVert:
624                         sel_verts = [sel]
625                     elif type(sel) is bmesh.types.BMEdge:
626                         sel_verts = sel.verts
627                     elif type(sel) is bmesh.types.BMFace:
628                         sel_verts = sel.verts
629                     for v in sel_verts:
630                         v_co3d = m_w * v.co
631                         add_pt(self, v_co3d)
632                         if self.pt_cnt > 2:
633                             exit_loop = True
634                             break
635                     if exit_loop:
636                         break
637
638
639 # todo : find way to merge this with add_select ?
640 def add_select_multi(self):
641     if self.multi_tmp.cnt < self.multi_tmp.max_cnt:
642         if bpy.context.mode == "OBJECT":
643             if len(bpy.context.selected_objects) > 0:
644                 for obj in bpy.context.selected_objects:
645                     self.multi_tmp.try_add(obj.location)
646                     if self.multi_tmp.cnt == self.multi_tmp.max_cnt:
647                         break
648         elif bpy.context.mode == "EDIT_MESH":
649             m_w = bpy.context.edit_object.matrix_world
650             bm = bmesh.from_edit_mesh(bpy.context.edit_object.data)
651             if len(bm.select_history) > 0:
652                 exit_loop = False  # simplify checking...
653                 for sel in bm.select_history:
654                     sel_verts = []
655                     if type(sel) is bmesh.types.BMVert:
656                         sel_verts = [sel]
657                     elif type(sel) is bmesh.types.BMEdge:
658                         sel_verts = sel.verts
659                     elif type(sel) is bmesh.types.BMFace:
660                         sel_verts = sel.verts
661                     for v in sel_verts:
662                         v_co3d = m_w * v.co
663                         self.multi_tmp.try_add(v_co3d)
664                         if self.multi_tmp.cnt == self.multi_tmp.max_cnt:
665                             exit_loop = True
666                             break
667                     if exit_loop:
668                         break
669         if in_ref_pts(self, self.multi_tmp.get_co(), self.mod_pt):
670             self.report({'WARNING'}, 'Points overlap.')
671         self.pts[self.mod_pt].co3d = self.multi_tmp.get_co()
672
673
674 def swap_ref_pts(self, pt1, pt2):
675     temp = self.pts[pt1].co3d.copy()
676     self.pts[pt1].co3d = self.pts[pt2].co3d.copy()
677     self.pts[pt2].co3d = temp
678
679
680 def set_meas_btn(self):
681     lock_pts = RotDat.lock_pts
682     if self.pt_cnt == 2:
683         global curr_meas_stor
684         curr_meas_stor = (lock_pts[0].co3d - lock_pts[1].co3d).length
685         self.meas_btn.set_text(format(curr_meas_stor, '.2f'))
686     elif self.pt_cnt == 3:
687         algn_co1 = lock_pts[0].co3d - lock_pts[2].co3d
688         algn_co3 = lock_pts[1].co3d - lock_pts[2].co3d
689         curr_meas_stor = degrees( algn_co1.angle(algn_co3) )
690         self.meas_btn.set_text(format(curr_meas_stor, '.2f'))
691         return
692
693
694 # For adding multi point without first needing a reference point
695 # todo : clean up TempPoint so this function isn't needed
696 # todo : find way to merge this with add_select_multi
697 def new_select_multi(self):
698     def enable_multi_mode(self):
699         if self.grab_pt is not None:
700             self.multi_tmp.__init__()
701             self.multi_tmp.co3d = Vector()
702             self.mod_pt = self.grab_pt
703             self.grab_pt = None
704         elif self.mod_pt is None:
705             self.multi_tmp.__init__()
706             self.multi_tmp.co3d = Vector()
707             self.mod_pt = self.pt_cnt
708             self.pt_cnt += 1
709
710     if bpy.context.mode == "OBJECT":
711         if len(bpy.context.selected_objects) > 0:
712             enable_multi_mode(self)
713             for obj in bpy.context.selected_objects:
714                 self.multi_tmp.try_add(obj.location)
715                 if self.multi_tmp.cnt == self.multi_tmp.max_cnt:
716                     break
717         else:
718             return
719     elif bpy.context.mode == "EDIT_MESH":
720         m_w = bpy.context.edit_object.matrix_world
721         bm = bmesh.from_edit_mesh(bpy.context.edit_object.data)
722         if len(bm.select_history) > 0:
723             enable_multi_mode(self)
724             exit_loop = False  # simplify checking...
725             for sel in bm.select_history:
726                 sel_verts = []
727                 if type(sel) is bmesh.types.BMVert:
728                     sel_verts = [sel]
729                 elif type(sel) is bmesh.types.BMEdge:
730                     sel_verts = sel.verts
731                 elif type(sel) is bmesh.types.BMFace:
732                     sel_verts = sel.verts
733                 for v in sel_verts:
734                     v_co3d = m_w * v.co
735                     self.multi_tmp.try_add(v_co3d)
736                     if self.multi_tmp.cnt == self.multi_tmp.max_cnt:
737                         exit_loop = True
738                         break
739                 if exit_loop:
740                     break
741         else:
742             return
743
744
745 def exit_multi_mode(self):
746     m_co3d = self.multi_tmp.get_co()
747     if in_ref_pts(self, m_co3d, self.mod_pt):
748         self.report({'ERROR'}, "Point overlapped another and was removed.")
749         rem_ref_pt(self, self.mod_pt)
750     else:
751         self.pts[self.mod_pt].co3d = m_co3d
752         if self.pt_cnt > 1:
753             updatelock_pts(self, self.pts)
754         set_mouse_highlight(self)
755     self.mod_pt = None
756     set_meas_btn(self)
757     set_help_text(self, "CLICK")
758
759
760 # Returns the closest object origin or vertex to the supplied 2D location
761 # as 3D Vector.
762 # Returns None if no found coordinate closer than minimum distance.
763 def find_closest_point(loc):
764     region = bpy.context.region
765     rv3d = bpy.context.region_data
766     shortest_dist = 40.0  # minimum distance from loc
767     closest = None
768     for obj in bpy.context.scene.objects:
769         o_co2d = loc3d_to_reg2d(region, rv3d, obj.location)
770         if o_co2d is None:
771             continue
772         dist2d = (loc - o_co2d).length
773         if dist2d < shortest_dist:
774             shortest_dist = dist2d
775             closest = obj.location.copy()
776         if obj.type == 'MESH':
777             if len(obj.data.vertices) > 0:
778                 for v in obj.data.vertices:
779                     v_co3d = obj.matrix_world * v.co
780                     v_co2d = loc3d_to_reg2d(region, rv3d, v_co3d)
781                     if v_co2d is not None:
782                         dist2d = (loc - v_co2d).length
783                         if dist2d < shortest_dist:
784                             shortest_dist = dist2d
785                             closest = v_co3d
786     return closest
787
788
789 def draw_pt_2d(pt_co, pt_color, pt_size):
790     if pt_co is not None:
791         bgl.glEnable(bgl.GL_BLEND)
792         bgl.glPointSize(pt_size)
793         bgl.glColor4f(*pt_color)
794         bgl.glBegin(bgl.GL_POINTS)
795         bgl.glVertex2f(*pt_co)
796         bgl.glEnd()
797     return
798
799
800 def draw_line_2d(pt_co_1, pt_co_2, pt_color):
801     if None not in (pt_co_1, pt_co_2):
802         bgl.glEnable(bgl.GL_BLEND)
803         bgl.glPointSize(15)
804         bgl.glColor4f(*pt_color)
805         bgl.glBegin(bgl.GL_LINE_STRIP)
806         bgl.glVertex2f(*pt_co_1)
807         bgl.glVertex2f(*pt_co_2)
808         bgl.glEnd()
809     return
810
811
812 def closest_to_point(pt, pts):
813     smallest_dist = 15.0
814     closest, pt_idx = None, None
815     for p in range(len(pts)):
816         if pts[p] is not None:
817             tmp_d = (pt - pts[p]).length
818             if tmp_d < smallest_dist:
819                 smallest_dist = tmp_d
820                 closest = pts[p]
821                 pt_idx = p
822     return closest, pt_idx
823
824
825 # Can a transformation be performed? Called after measure button is clicked
826 # to let user know if valid options are set before enabling pop-up to get
827 # user input.
828 # todo, move transf_type assignment to "point add" part of code?
829 def can_transf(self):
830     global curr_meas_stor
831     success = False
832     if self.pt_cnt == 2:
833         mode = self.menu.get_mode()
834         if mode == "Move":
835             self.transf_type = MOVE
836             success = True
837         elif mode == "Scale":
838             self.transf_type = SCALE
839             success = True
840
841     elif self.pt_cnt == 3:
842         self.transf_type = ROTATE
843         if RotDat.axis_lock is not None:
844             success = True
845         # if not flat angle and no axis lock set, begin preparations for
846         # arbitrary axis / spherical rotation
847         elif not flts_alm_eq(curr_meas_stor, 0.0) and \
848         not flts_alm_eq(curr_meas_stor, 180.0):
849             fre_co = self.pts[0].co3d
850             anc_co = self.pts[1].co3d
851             piv_co = self.pts[2].co3d
852             RotDat.piv_norm = geometry.normal(anc_co, piv_co, fre_co)
853             success = True
854         else:
855             # would need complex angle processing workaround to get
856             # spherical rotations working with flat angles. todo item?
857             # blocking execution for now.
858             self.report({'INFO'}, "Need axis lock for 0 and 180 degree angles.")
859     return success
860
861
862 # For making sure rise over run doesn't get flipped.
863 def slope_check(pt1, pt2):
864     cmp_ls = []
865     for i in range(len(pt1)):
866         cmp_ls.append(flts_alm_eq(pt1[i], pt2[i]) or pt1[i] > pt2[i])
867     return cmp_ls
868
869
870 # Finds 3D location that shares same slope of line connecting Anchor and
871 # Free or that is on axis line going through Anchor.
872 def get_new_3d_co(self, old_dis, new_dis):
873     pt_anc, pt_fr = self.pts[1].co3d, self.pts[0].co3d
874     if RotDat.axis_lock is None:
875         if new_dis == 0:
876             return pt_anc
877         orig_slope = slope_check(pt_anc, pt_fr)
878         scale = new_dis / old_dis
879         pt_pos = pt_anc.lerp(pt_fr,  scale)
880         pt_neg = pt_anc.lerp(pt_fr, -scale)
881         pt_pos_slp = slope_check(pt_anc, pt_pos)
882         pt_neg_slp = slope_check(pt_anc, pt_neg)
883         # note: slope_check returns 3 bool values
884         if orig_slope == pt_pos_slp:
885             if new_dis > 0:
886                 return pt_pos
887             else:
888                 # for negative distances
889                 return pt_neg
890         elif orig_slope == pt_neg_slp:
891             if new_dis > 0:
892                 return pt_neg
893             else:
894                 return pt_pos
895         else:  # neither slope matches
896             self.report({'ERROR'}, 'Slope mismatch. Cannot calculate new point.')
897             return None
898
899     elif RotDat.axis_lock == 'X':
900         if pt_fr[0] > pt_anc[0]:
901             return Vector([ pt_anc[0] + new_dis, pt_fr[1], pt_fr[2] ])
902         else:
903             return Vector([ pt_anc[0] - new_dis, pt_fr[1], pt_fr[2] ])
904     elif RotDat.axis_lock == 'Y':
905         if pt_fr[1] > pt_anc[1]:
906             return Vector([ pt_fr[0], pt_anc[1] + new_dis, pt_fr[2] ])
907         else:
908             return Vector([ pt_fr[0], pt_anc[1] - new_dis, pt_fr[2] ])
909     elif RotDat.axis_lock == 'Z':
910         if pt_fr[2] > pt_anc[2]:
911             return Vector([ pt_fr[0], pt_fr[1], pt_anc[2] + new_dis ])
912         else:
913             return Vector([ pt_fr[0], pt_fr[1], pt_anc[2] - new_dis ])
914     else:  # neither slope matches
915         self.report({'ERROR'}, "Slope mismatch. Can't calculate new point.")
916         return None
917
918
919 def set_arc_pts(ref_pts):
920     fre, anc, piv = ref_pts[0].co3d, ref_pts[1].co3d, ref_pts[2].co3d
921     arc_pts = []
922     ang = (fre - piv).angle(anc - piv)
923     deg_ang = degrees(ang)
924     if deg_ang > 0.01 and deg_ang < 179.99:
925         piv_norm = geometry.normal(fre, piv, anc)
926         rot_val = Quaternion(piv_norm, ang)
927         rotated = fre - piv
928         rotated.rotate(rot_val)
929         rotated += piv
930         rot_ang = (anc - piv).angle(rotated - piv)
931         if not flts_alm_eq(rot_ang, 0.0):
932             ang = -ang
933         dis_p_f = (piv - fre).length
934         dis_p_a = (piv - anc).length
935         if dis_p_f < dis_p_a:
936             ratio = 0.5
937         else:  # dis_p_a < dis_p_f:
938             ratio = dis_p_a / dis_p_f * 0.5
939         mid_piv_free = piv.lerp(fre, ratio)
940         arc_pts = [mid_piv_free]
941         steps = abs( int(degrees(ang) // 10) )
942         ang_step = ang / steps
943         mid_align = mid_piv_free - piv
944         for a in range(1, steps):
945             rot_val = Quaternion(piv_norm, ang_step * a)
946             temp = mid_align.copy()
947             temp.rotate(rot_val)
948             arc_pts.append(temp + piv)
949         # in case steps <= 1
950         rot_val = Quaternion(piv_norm, ang)
951         temp = mid_align.copy()
952         temp.rotate(rot_val)
953         arc_pts.append(temp + piv)
954
955     elif RotDat.axis_lock is not None:
956         #if RotDat.axis_lock == 'X':
957         #    rot_val = Euler((pi*2, 0.0, 0.0), 'XYZ')
958         if RotDat.axis_lock == 'X':
959             piv_norm = 1.0, 0.0, 0.0
960         elif RotDat.axis_lock == 'Y':
961             piv_norm = 0.0, 1.0, 0.0
962         elif RotDat.axis_lock == 'Z':
963             piv_norm = 0.0, 0.0, 1.0
964         dis_p_f = (piv - fre).length
965         dis_p_a = (piv - anc).length
966         if dis_p_f < dis_p_a:
967             ratio = 0.5
968         else:  # dis_p_a < dis_p_f:
969             ratio = dis_p_a / dis_p_f * 0.5
970         mid_piv_free = piv.lerp(fre, ratio)
971         arc_pts = [mid_piv_free]
972         steps = 36
973         ang_step = pi*2 / steps
974         mid_align = mid_piv_free - piv
975         for a in range(1, steps+1):
976             rot_val = Quaternion(piv_norm, ang_step * a)
977             temp = mid_align.copy()
978             temp.rotate(rot_val)
979             arc_pts.append(temp + piv)
980
981     RotDat.arc_pts = arc_pts
982
983
984 # Takes a ref_pts (ReferencePoints class) argument and modifies its member
985 # variable lp_ls (lock pt list). The lp_ls variable is assigned a modified list
986 # of 3D coordinates (if an axis lock was provided), the contents of the
987 # ref_pts' rp_ls var (if no axis lock was provided), or an empty list (if there
988 # wasn't enough ref_pts or there was a problem creating the modified list).
989 # todo : move inside ReferencePoints class ?
990 def set_lock_pts(ref_pts, pt_cnt):
991     if pt_cnt < 2:
992         RotDat.lock_pts = []
993     elif RotDat.axis_lock is None:
994         RotDat.lock_pts = ref_pts
995         if pt_cnt == 3:
996             set_arc_pts(ref_pts)
997     else:
998         RotDat.lock_pts = []
999         new1 = ref_pts[1].copy()
1000         ptls = [ref_pts[i].co3d for i in range(pt_cnt)]  # shorthand
1001         # finds 3D midpoint between 2 supplied coordinates
1002         # axis determines which coordinates are assigned midpoint values
1003         # if X, Anchor is [AncX, MidY, MidZ] and Free is [FreeX, MidY, MidZ]
1004         if pt_cnt == 2:  # translate
1005             new0 = ref_pts[0].copy()
1006             mid3d = ptls[0].lerp(ptls[1], 0.5)
1007             if RotDat.axis_lock == 'X':
1008                 new0.co3d = Vector([ ptls[0][0], mid3d[1], mid3d[2] ])
1009                 new1.co3d = Vector([ ptls[1][0], mid3d[1], mid3d[2] ])
1010             elif RotDat.axis_lock == 'Y':
1011                 new0.co3d = Vector([ mid3d[0], ptls[0][1], mid3d[2] ])
1012                 new1.co3d = Vector([ mid3d[0], ptls[1][1], mid3d[2] ])
1013             elif RotDat.axis_lock == 'Z':
1014                 new0.co3d = Vector([ mid3d[0], mid3d[1], ptls[0][2] ])
1015                 new1.co3d = Vector([ mid3d[0], mid3d[1], ptls[1][2] ])
1016             if not vec3s_alm_eq(new0.co3d, new1.co3d):
1017                 RotDat.lock_pts = [new0, new1]
1018
1019         # axis determines which of the Free's coordinates are assigned
1020         # to Anchor and Pivot coordinates eg:
1021         # if X, Anchor is [FreeX, AncY, AncZ] and Pivot is [FreeX, PivY, PivZ]
1022         elif pt_cnt == 3:  # rotate
1023             new2 = ref_pts[2].copy()
1024             mov_co = ref_pts[0].co3d.copy()
1025             if RotDat.axis_lock == 'X':
1026                 new1.co3d = Vector([ mov_co[0], ptls[1][1], ptls[1][2] ])
1027                 new2.co3d = Vector([ mov_co[0], ptls[2][1], ptls[2][2] ])
1028             elif RotDat.axis_lock == 'Y':
1029                 new1.co3d = Vector([ ptls[1][0], mov_co[1], ptls[1][2] ])
1030                 new2.co3d = Vector([ ptls[2][0], mov_co[1], ptls[2][2] ])
1031             elif RotDat.axis_lock == 'Z':
1032                 new1.co3d = Vector([ ptls[1][0], ptls[1][1], mov_co[2] ])
1033                 new2.co3d = Vector([ ptls[2][0], ptls[2][1], mov_co[2] ])
1034             if not vec3s_alm_eq(new1.co3d, new2.co3d) and \
1035             not vec3s_alm_eq(new1.co3d, mov_co) and \
1036             not vec3s_alm_eq(new2.co3d, mov_co):
1037                 #new0 = ReferencePoint("piv", Colr.blue, mov_co)
1038                 new0 = ReferencePoint("fre", Colr.green, mov_co)
1039                 RotDat.lock_pts = [new0, new1, new2]
1040                 set_arc_pts([new0, new1, new2])
1041             else:
1042                 set_arc_pts(ref_pts)
1043
1044
1045 # Takes  new_co (Vector) and old_co (Vector) as arguments. Calculates
1046 # difference between the 3D locations in new_co and old_co to determine
1047 # the translation to apply to the selected geometry.
1048 def do_translation(new_co, old_co):
1049     co_chg = -(old_co - new_co)  # co_chg = coordinate change
1050     bpy.ops.transform.translate(value=(co_chg[0], co_chg[1], co_chg[2]))
1051
1052
1053 # Performs a scale transformation using the provided s_fac (scale factor)
1054 # argument. The scale factor is the result from dividing the user input
1055 # measure (new_meas_stor) by the distance between the Anchor and Free
1056 # (curr_meas_stor). After the scale is performed, settings are returned to
1057 # their "pre-scaled" state.
1058 # takes:  ref_pts (ReferencePoints), s_fac (float)
1059 def do_scale(ref_pts, s_fac):
1060     # back up settings before changing them
1061     piv_back = deepcopy(bpy.context.space_data.pivot_point)
1062     curs_back = bpy.context.scene.cursor.location.copy()
1063     bpy.context.space_data.pivot_point = 'CURSOR'
1064     bpy.context.scene.cursor.location = ref_pts[1].co3d.copy()
1065     ax_multip, cnstrt_bls = (), ()
1066     if   RotDat.axis_lock is None:
1067         ax_multip, cnstrt_bls = (s_fac, s_fac, s_fac), (True, True, True)
1068     elif RotDat.axis_lock == 'X':
1069         ax_multip, cnstrt_bls = (s_fac, 1, 1), (True, False, False)
1070     elif RotDat.axis_lock == 'Y':
1071         ax_multip, cnstrt_bls = (1, s_fac, 1), (False, True, False)
1072     elif RotDat.axis_lock == 'Z':
1073         ax_multip, cnstrt_bls = (1, 1, s_fac), (False, False, True)
1074     bpy.ops.transform.resize(value=ax_multip, constraint_axis=cnstrt_bls)
1075     # restore settings back to their pre "do_scale" state
1076     bpy.context.scene.cursor.location = curs_back.copy()
1077     bpy.context.space_data.pivot_point = deepcopy(piv_back)
1078
1079
1080 # end_a, piv_pt, and end_b are Vector based 3D coordinates
1081 # coordinates must share a common center "pivot" point (piv_pt)
1082 def get_line_ang_3d(end_a, piv_pt, end_b):
1083     algn_a = end_a - piv_pt
1084     algn_b = end_b - piv_pt
1085     return algn_a.angle(algn_b)
1086
1087
1088 # Checks if the 3 Vector coordinate arguments (end_a, piv_pt, end_b)
1089 # will create an angle with a measurement matching the value in the
1090 # argument exp_ang (expected angle measurement).
1091 def ang_match3d(end_a, piv_pt, end_b, exp_ang):
1092     ang_meas = get_line_ang_3d(end_a, piv_pt, end_b)
1093     #print("end_a", end_a)  # debug
1094     #print("piv_pt", piv_pt)  # debug
1095     #print("end_b", end_b)  # debug
1096     #print("exp_ang ", exp_ang)  # debug
1097     #print("ang_meas ", ang_meas)  # debug
1098     return flts_alm_eq(ang_meas, exp_ang)
1099
1100
1101 # Calculates rotation around axis or face normal at Pivot's location.
1102 # Takes two 3D coordinate Vectors (piv_co and mov_co), rotation angle in
1103 # radians (ang_diff_rad), and rotation data storage object (rot_dat).
1104 # Aligns mov_co to world origin (0, 0, 0) and rotates aligned
1105 # mov_co (mov_aligned) around axis stored in rot_dat. After rotation,
1106 # removes world-origin alignment.
1107 def get_rotated_pt(piv_co, ang_diff_rad, mov_co):
1108     mov_aligned = mov_co - piv_co
1109     rot_val, axis_lock = [], RotDat.axis_lock
1110     if   axis_lock is None:  # arbitrary axis / spherical rotations
1111         rot_val = Quaternion(RotDat.piv_norm, ang_diff_rad)
1112     elif axis_lock == 'X':
1113         rot_val = Euler((ang_diff_rad, 0.0, 0.0), 'XYZ')
1114     elif axis_lock == 'Y':
1115         rot_val = Euler((0.0, ang_diff_rad, 0.0), 'XYZ')
1116     elif axis_lock == 'Z':
1117         rot_val = Euler((0.0, 0.0, ang_diff_rad), 'XYZ')
1118     mov_aligned.rotate(rot_val)
1119     return mov_aligned + piv_co
1120
1121
1122 # Finds out whether positive RotDat.new_ang_r or negative RotDat.new_ang_r
1123 # will result in the desired rotation angle.
1124 def find_correct_rot(ref_pts, pt_cnt):
1125     ang_diff_rad, new_ang_rad = RotDat.ang_diff_r, RotDat.new_ang_r
1126     piv_pt, move_pt = ref_pts[2].co3d, ref_pts[0].co3d
1127
1128     t_co_pos = get_rotated_pt(piv_pt, ang_diff_rad, move_pt)
1129     t_co_neg = get_rotated_pt(piv_pt,-ang_diff_rad, move_pt)
1130     set_lock_pts(ref_pts, pt_cnt)
1131     lock_pts = RotDat.lock_pts
1132     if ang_match3d(lock_pts[1].co3d, lock_pts[2].co3d, t_co_pos, new_ang_rad):
1133         #print("matched t_co_pos:", t_co_pos, ang_diff_rad)
1134         return t_co_pos, ang_diff_rad
1135     else:
1136         #print("matched t_co_neg:", t_co_neg, -ang_diff_rad)
1137         return t_co_neg, -ang_diff_rad
1138
1139
1140 # Takes 2D Pivot Point (piv) for piv to temp lines, 2 possible rotation
1141 # coordinates to choose between (rot_co_pos, rot_co_neg), and a
1142 # 2D mouse location (mouse_co) for determining which rotation coordinate
1143 # is closest to the cursor.
1144 # Returns the rotation coordinate closest to the 2d mouse position and the
1145 # rotation angles used to obtain the coordinates (rot_ang_rad).
1146 # rot_co_pos == rotated coordinate positive,  rot_co_neg == rot coor Negative
1147 # todo : make r_p_co2d and r_n_co2d VertObj types ?
1148 #def choose_0_or_180(piv, rot_co_pos, r_p_ang_r, rot_co_neg, r_n_ang_r, mouse_co):
1149 def choose_0_or_180(piv, rot_co_pos, rot_co_neg, rot_ang_rad, mouse_co):
1150     #global reg_rv3d
1151     #region, rv3d = reg_rv3d[0], reg_rv3d[1]
1152     region = bpy.context.region
1153     rv3d = bpy.context.region_data
1154     r_p_co2d = loc3d_to_reg2d(region, rv3d, rot_co_pos)
1155     r_n_co2d = loc3d_to_reg2d(region, rv3d, rot_co_neg)
1156     piv2d = loc3d_to_reg2d(region, rv3d, piv.co3d)
1157     ms_co_1_dis = (r_p_co2d - mouse_co).length
1158     ms_co_2_dis = (r_n_co2d - mouse_co).length
1159     # draw both buttons and show which is closer to mouse
1160     psize_small, psize_large = 8, 14
1161     if   ms_co_1_dis < ms_co_2_dis:
1162         draw_line_2d(piv2d, r_p_co2d, Colr.green)
1163         draw_pt_2d(r_p_co2d, Colr.green, psize_large)
1164         draw_pt_2d(r_n_co2d, Colr.grey, psize_small)
1165         return rot_co_pos, rot_ang_rad
1166     elif ms_co_2_dis < ms_co_1_dis:
1167         draw_line_2d(piv2d, r_n_co2d, Colr.green)
1168         draw_pt_2d(r_n_co2d, Colr.green, psize_large)
1169         draw_pt_2d(r_p_co2d, Colr.grey, psize_small)
1170         return rot_co_neg, -rot_ang_rad
1171     else:
1172         draw_pt_2d(r_p_co2d, Colr.grey, psize_small)
1173         draw_pt_2d(r_n_co2d, Colr.grey, psize_small)
1174     return None, None
1175
1176
1177 # Reduces the provided rotation amount (new_ms_stor) to an "equivalent" value
1178 # less than or equal to 180 degrees. Calculates the angle offset from
1179 # curr_ms_stor to achieve a new_ms_stor value.
1180 def prep_rotation_info(curr_ms_stor, new_ms_stor):
1181     # workaround for negative angles and angles over 360 degrees
1182     if new_ms_stor < 0 or new_ms_stor > 360:
1183         new_ms_stor = new_ms_stor % 360
1184     # fix for angles over 180 degrees
1185     if new_ms_stor > 180:
1186         RotDat.new_ang_r = radians(180 - (new_ms_stor % 180))
1187     else:
1188         RotDat.new_ang_r = radians(new_ms_stor)
1189     #print("RotDat.new_ang_r", RotDat.new_ang_r)
1190     RotDat.ang_diff_r = radians(new_ms_stor - curr_ms_stor)
1191
1192
1193 # Uses axis_lock or piv_norm from RotDat to obtain rotation axis.
1194 # Then rotates selected objects or selected vertices around the
1195 # 3D cursor using RotDat's ang_diff_r radian value.
1196 def do_rotate(self):
1197     # back up settings before changing them
1198     piv_back = deepcopy(bpy.context.space_data.pivot_point)
1199     curs_back = bpy.context.scene.cursor.location.copy()
1200     bpy.context.space_data.pivot_point = 'CURSOR'
1201     bpy.context.scene.cursor.location = self.pts[2].co3d.copy()
1202     
1203     axis_lock = RotDat.axis_lock
1204     ops_lock = ()  # axis lock data for bpy.ops.transform
1205     if   axis_lock is None: ops_lock = RotDat.piv_norm
1206     elif axis_lock == 'X':  ops_lock = 1, 0, 0
1207     elif axis_lock == 'Y':  ops_lock = 0, 1, 0
1208     elif axis_lock == 'Z':  ops_lock = 0, 0, 1
1209
1210     if bpy.context.mode == "OBJECT":
1211         bpy.ops.transform.rotate(value=RotDat.ang_diff_r, axis=ops_lock,
1212                 constraint_axis=(False, False, False))
1213
1214     elif bpy.context.mode == "EDIT_MESH":
1215         bpy.ops.transform.rotate(value=RotDat.ang_diff_r, axis=ops_lock,
1216                 constraint_axis=(False, False, False))
1217         editmode_refresh()
1218
1219     # restore settings back to their pre "do_rotate" state
1220     bpy.context.scene.cursor.location = curs_back.copy()
1221     bpy.context.space_data.pivot_point = deepcopy(piv_back)
1222
1223
1224 # Updates lock points and changes curr_meas_stor to use measure based on
1225 # lock points instead of ref_pts (for axis constrained transformations).
1226 def updatelock_pts(self, ref_pts):
1227     global curr_meas_stor
1228     set_lock_pts(ref_pts, self.pt_cnt)
1229     if RotDat.lock_pts == []:
1230         if RotDat.axis_lock is not None:
1231             self.report({'ERROR'}, 'Axis lock \''+ RotDat.axis_lock+
1232                     '\' creates identical points')
1233         RotDat.lock_pts = ref_pts
1234         RotDat.axis_lock = None
1235     # update Measurement in curr_meas_stor
1236     lk_pts = RotDat.lock_pts
1237     if self.pt_cnt < 2:
1238         curr_meas_stor = 0.0
1239     elif self.pt_cnt == 2:
1240         curr_meas_stor = (lk_pts[0].co3d - lk_pts[1].co3d).length
1241     elif self.pt_cnt == 3:
1242         line_ang_r = get_line_ang_3d(lk_pts[1].co3d, lk_pts[2].co3d, lk_pts[0].co3d)
1243         curr_meas_stor = degrees(line_ang_r)
1244
1245
1246 # See if key was pressed that would require updating the axis lock info.
1247 # If one was, update the lock points to use new info.
1248 def axis_key_check(self, new_axis):
1249     if self.pt_cnt > 1:
1250         if new_axis != RotDat.axis_lock:
1251             RotDat.axis_lock = new_axis
1252             updatelock_pts(self, self.pts)
1253             set_meas_btn(self)
1254
1255
1256 # Adjusts settings so proc_click can run again for next possible transform
1257 def reset_settings(self):
1258     #print("reset_settings")  # debug
1259     global new_meas_stor
1260     new_meas_stor = None
1261     self.new_free_co = ()
1262     self.mouse_co = Vector((-9900, -9900))
1263     editmode_refresh()
1264     if self.pt_cnt < 2:
1265         self.meas_btn.is_drawn = False
1266         set_lock_pts(self.pts, self.pt_cnt)
1267     else:
1268         updatelock_pts(self, self.pts)
1269         self.meas_btn.is_drawn = True
1270         set_meas_btn(self)
1271     #self.snap_btn_act = True
1272     self.addon_mode = CLICK_CHECK
1273
1274     # restore selected items (except Anchor)
1275     # needed so GRABONLY and SLOW3DTO2D update selection correctly
1276     #self.sel_backup.restore_selected()
1277
1278     # make sure last transform didn't cause points to overlap
1279     if vec3s_alm_eq(self.pts[0].co3d, self.pts[1].co3d):
1280         self.report({'ERROR'}, 'Free and Anchor share same location.')
1281         # reset ref pt data
1282         self.pt_cnt = 0
1283         self.menu.change_menu(self.pt_cnt)
1284         init_ref_pts(self)
1285         self.highlight_mouse = True
1286
1287     #if self.pt_find_md == GRABONLY:
1288     #    create_snap_pt(self.left_click_co, self.sel_backup)
1289
1290
1291 # runs transformation functions depending on which options are set.
1292 # transform functions cannot be called directly due to use of pop-up for
1293 # getting user input
1294 def do_transform(self):
1295     #print("do_transform")  # debug
1296     global curr_meas_stor, new_meas_stor
1297
1298     # Onto Transformations...
1299     if self.transf_type == MOVE:
1300         new_coor = get_new_3d_co(self, curr_meas_stor, new_meas_stor)
1301         if new_coor is not None:
1302             do_translation(new_coor, self.pts[0].co3d)
1303             self.pts[0].co3d = new_coor.copy()
1304         reset_settings(self)
1305
1306     elif self.transf_type == SCALE:
1307         #print("SCALE!!")  # debug
1308         new_coor = get_new_3d_co(self, curr_meas_stor, new_meas_stor)
1309         if new_coor is not None:
1310             scale_factor = new_meas_stor / curr_meas_stor
1311             do_scale(self.pts, scale_factor)
1312             self.pts[0].co3d = new_coor.copy()
1313         reset_settings(self)
1314
1315     elif self.transf_type == ROTATE:
1316         if self.new_free_co != ():
1317             do_rotate(self)
1318             self.pts[0].co3d = self.new_free_co.copy()
1319         reset_settings(self)
1320
1321
1322 # Run after for XEditMeasureInputPanel pop-up disables popup_active.
1323 # Checks to see if a valid number was input into the pop-up dialog and
1324 # determines what to do based on what the pop-up was supplied (if anything).
1325 def process_popup_input(self):
1326     global curr_meas_stor, new_meas_stor
1327     #print("process_popup_input")  # debug
1328     #print("curr_meas_stor", curr_meas_stor, " new_meas_stor", new_meas_stor)  # debug
1329     if new_meas_stor is not None:
1330         self.addon_mode = DO_TRANSFORM
1331         if self.transf_type == MOVE:
1332             do_transform(self)
1333         elif self.transf_type == SCALE:
1334             do_transform(self)
1335         elif self.transf_type == ROTATE:
1336             prep_rotation_info(curr_meas_stor, new_meas_stor)
1337             # if angle is flat...
1338             if flts_alm_eq(curr_meas_stor, 0.0) or \
1339             flts_alm_eq(curr_meas_stor, 180.0):
1340                 piv, mov = self.pts[2].co3d, self.pts[0].co3d
1341                 ang_rad = RotDat.ang_diff_r
1342                 if flts_alm_eq(new_meas_stor, 0.0) or \
1343                 flts_alm_eq(new_meas_stor, 180.0):
1344                     self.new_free_co = get_rotated_pt(piv, ang_rad, mov)
1345                     do_transform(self)
1346                 else:
1347                     RotDat.rot_pt_pos = get_rotated_pt(piv, ang_rad, mov)
1348                     RotDat.rot_pt_neg = get_rotated_pt(piv, -ang_rad, mov)
1349                     self.addon_mode = GET_0_OR_180
1350             else:  # non-flat angle
1351                 self.new_free_co, RotDat.ang_diff_r = \
1352                         find_correct_rot(self.pts, self.pt_cnt)
1353                 do_transform(self)
1354     else:
1355         reset_settings(self)
1356
1357
1358 def draw_rot_arc(colr):
1359     reg = bpy.context.region
1360     rv3d = bpy.context.region_data
1361     len_arc_pts = len(RotDat.arc_pts)
1362     if len_arc_pts > 1:
1363         last = loc3d_to_reg2d(reg, rv3d, RotDat.arc_pts[0])
1364         for p in range(1, len_arc_pts):
1365             p2d = loc3d_to_reg2d(reg, rv3d, RotDat.arc_pts[p])
1366             draw_line_2d(last, p2d, Colr.white)
1367             last = p2d
1368
1369
1370 # Called when add-on mode changes and every time point is added or removed.
1371 def set_help_text(self, mode):
1372     text = ""
1373     if mode == "CLICK":
1374         if self.pt_cnt == 0:
1375             text = "ESC/LMB+RMB - exits add-on, LMB - add ref point"
1376         elif self.pt_cnt == 1:
1377             text = "ESC/LMB+RMB - exits add-on, LMB - add/remove ref points, G - grab point, SHIFT+LMB enter mid point mode"
1378         elif self.pt_cnt == 2:
1379             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, UP/DOWN - change tranform mode"
1380         else:  # self.pt_cnt == 3
1381             text = "ESC/LMB+RMB - exits add-on, LMB - remove ref points, X/Y/Z - set axis lock, C - clear axis lock, G - grab point, SHIFT+LMB enter mid point mode, UP/DOWN - change tranform mode"
1382     elif mode == "MULTI":
1383         text = "ESC/LMB+RMB - exits add-on, SHIFT+LMB exit mid point mode, LMB - add/remove point"
1384     elif mode == "GRAB":
1385         text = "ESC/LMB+RMB - exits add-on, G - cancel grab, LMB - place/swap ref points"
1386     elif mode == "POPUP":
1387         text = "ESC/LMB+RMB - exits add-on, LMB/RMB (outside pop-up) - cancel pop-up input"
1388
1389     bpy.context.area.header_text_set(text)
1390
1391
1392 # todo : move most of below to mouse_co update in modal?
1393 def draw_callback_px(self, context):
1394     reg = bpy.context.region
1395     rv3d = bpy.context.region_data
1396     ptsz_lrg = 20
1397     ptsz_sml = 10
1398
1399     add_rm_co = Vector((self.rtoolsw, 0))
1400     self.add_rm_btn.draw_btn(add_rm_co, self.mouse_co, self.shift_held)
1401
1402     # allow appending None so indexing does not get messed up
1403     # causing potential false positive for overlap
1404     pts2d = [p.get_co2d() for p in self.pts]
1405     ms_colr = Colr.yellow
1406     if self.pt_cnt < 3:
1407         ms_colr = self.pts[self.pt_cnt].colr
1408
1409     lk_pts2d = None  # lock points 2D
1410     self.meas_btn.is_drawn = False  # todo : cleaner btn activation
1411
1412     # if the addon_mode is WAIT_FOR_POPUP, wait on POPUP to disable
1413     # popup_active, then run process_popup_input
1414     # would prefer not to do pop-up check inside draw_callback, but not sure
1415     # how else to check for input. need higher level "input handler" class?
1416     if self.addon_mode == WAIT_FOR_POPUP:
1417         global popup_active
1418         if not popup_active:
1419             process_popup_input(self)
1420             set_help_text(self, "CLICK")
1421         
1422     elif self.addon_mode == GET_0_OR_180:
1423         choose_0_or_180(RotDat.lock_pts[2], RotDat.rot_pt_pos,
1424                 RotDat.rot_pt_neg, RotDat.ang_diff_r, self.mouse_co)
1425
1426     # note, can't chain above if-elif block in with one below as
1427     # it breaks axis lock drawing
1428     if self.grab_pt is not None:  # not enabled if mod_pt active
1429         line_beg = pts2d[self.grab_pt]  # backup orignal co for move line
1430         pts2d[self.grab_pt] = None  # prevent check on grabbed pt
1431         closest_pt, self.overlap_idx = closest_to_point(self.mouse_co, pts2d)
1432         pts2d[self.grab_pt] = self.mouse_co
1433         ms_colr = self.pts[self.grab_pt].colr
1434         if not self.shift_held:
1435             draw_line_2d(line_beg, self.mouse_co, self.pts[self.grab_pt].colr)
1436             draw_pt_2d(closest_pt, Colr.white, ptsz_lrg)
1437
1438     elif self.mod_pt is not None:
1439         ms_colr = self.pts[self.mod_pt].colr
1440         m_pts2d = [loc3d_to_reg2d(reg, rv3d, p) for p in self.multi_tmp.ls]
1441         closest_pt, self.overlap_idx = closest_to_point(self.mouse_co, m_pts2d)
1442         draw_pt_2d(pts2d[self.mod_pt], Colr.white, ptsz_lrg)
1443         if self.shift_held:
1444             draw_pt_2d(self.mouse_co, Colr.black, ptsz_lrg)
1445             if len(m_pts2d) > 1:
1446                 for mp in m_pts2d:
1447                     draw_pt_2d(mp, Colr.black, ptsz_lrg)
1448         else:
1449             draw_pt_2d(closest_pt, Colr.black, ptsz_lrg)
1450         if len(m_pts2d) > 1:
1451             for p in m_pts2d:
1452                 draw_pt_2d(p, ms_colr, ptsz_sml)
1453         last_mod_pt = loc3d_to_reg2d(reg, rv3d, self.multi_tmp.ls[-1])
1454         draw_line_2d(last_mod_pt, self.mouse_co, self.pts[self.mod_pt].colr)
1455
1456     else:  # "Normal" mode
1457         closest_pt, self.overlap_idx = closest_to_point(self.mouse_co, pts2d)
1458         lin_p = pts2d
1459         if self.shift_held:
1460             draw_pt_2d(closest_pt, Colr.white, ptsz_lrg)
1461         else:
1462             draw_pt_2d(closest_pt, Colr.black, ptsz_lrg)
1463         if RotDat.axis_lock is not None:
1464             lk_pts2d = [p.get_co2d() for p in RotDat.lock_pts]
1465             lin_p = lk_pts2d
1466             # draw axis lock indicator
1467             if   RotDat.axis_lock == 'X':
1468                 txt_colr = Colr.red
1469             elif RotDat.axis_lock == 'Y':
1470                 txt_colr = Colr.green
1471             elif RotDat.axis_lock == 'Z':
1472                 txt_colr = Colr.blue
1473             dpi = bpy.context.preferences.system.dpi
1474             font_id, txt_sz = 0, 32
1475             x_pos, y_pos = self.rtoolsw + 80, 36
1476             bgl.glColor4f(*txt_colr)
1477             blf.size(font_id, txt_sz, dpi)
1478             blf.position(font_id, x_pos, y_pos, 0)
1479             blf.draw(font_id, RotDat.axis_lock)
1480         if self.pt_cnt == 2:
1481             draw_line_2d(lin_p[0], lin_p[1], Colr.white)
1482             if None not in (lin_p[0], lin_p[1]):
1483                 btn_co = lin_p[0].lerp(lin_p[1], 0.5)
1484                 self.meas_btn.draw_btn(btn_co, self.mouse_co)
1485                 self.meas_btn.is_drawn = True
1486         elif self.pt_cnt == 3:
1487             draw_rot_arc(self.pts[2].colr)
1488             draw_line_2d(lin_p[0], lin_p[2], Colr.white)
1489             draw_line_2d(lin_p[1], lin_p[2], Colr.white)
1490             self.meas_btn.draw_btn(lin_p[2], self.mouse_co)
1491             self.meas_btn.is_drawn = True
1492
1493     # draw reference points
1494     for p in range(self.pt_cnt):
1495         draw_pt_2d(pts2d[p], self.pts[p].colr, ptsz_sml)
1496
1497     # draw lock points
1498     if lk_pts2d is not None:
1499         lp_cnt = len(RotDat.lock_pts)
1500         for p in range(lp_cnt):
1501             draw_pt_2d(lk_pts2d[p], self.pts[p].colr, ptsz_sml)
1502
1503     if self.highlight_mouse:
1504         draw_pt_2d(self.mouse_co, ms_colr, ptsz_sml)
1505
1506     # draw mode selection menu
1507     self.menu.draw(self.meas_btn.is_drawn)
1508
1509
1510 def exit_addon(self):
1511     restore_blender_settings(self.settings_backup)
1512     bpy.context.area.header_text_set()
1513     # todo : reset openGL settings?
1514     #bgl.glColor4f()
1515     #blf.size()
1516     #blf.position()
1517     #print("\n\nAdd-On Exited\n")  # debug
1518
1519
1520 # Sees if "use_region_overlap" is enabled and X offset is needed.
1521 def get_reg_overlap():
1522     rtoolsw = 0  # region tools (toolbar) width
1523     #ruiw = 0  # region ui (Number/n-panel) width
1524     system = bpy.context.preferences.system
1525     if system.use_region_overlap:
1526         area = bpy.context.area
1527         for r in area.regions:
1528             if r.type == 'TOOLS':
1529                 rtoolsw = r.width
1530                 #elif r.type == 'UI':
1531                 #    ruiw = r.width
1532     #return rtoolsw, ruiw
1533     return rtoolsw
1534
1535
1536 class XEditSetMeas(bpy.types.Operator):
1537     bl_idname = "view3d.xedit_set_meas_op"
1538     bl_label = "XEdit Set Measaure"
1539
1540     # Only launch Add-On from OBJECT or EDIT modes
1541     @classmethod
1542     def poll(self, context):
1543         return context.mode == 'OBJECT' or context.mode == 'EDIT_MESH'
1544
1545     def modal(self, context, event):
1546         context.area.tag_redraw()
1547
1548         if event.type in {'A', 'MIDDLEMOUSE', 'WHEELUPMOUSE',
1549         'WHEELDOWNMOUSE', 'NUMPAD_1', 'NUMPAD_2', 'NUMPAD_3', 'NUMPAD_4',
1550         'NUMPAD_6', 'NUMPAD_7', 'NUMPAD_8', 'NUMPAD_9', 'NUMPAD_0', 'TAB'}:
1551             return {'PASS_THROUGH'}
1552
1553         if event.type == 'MOUSEMOVE':
1554             self.mouse_co = Vector((event.mouse_region_x, event.mouse_region_y))
1555
1556         if event.type in {'LEFT_SHIFT', 'RIGHT_SHIFT'}:
1557             if event.value == 'PRESS':
1558                 self.shift_held = True
1559                 #print("\nShift pressed")  # debug
1560             elif event.value == 'RELEASE':
1561                 self.shift_held = False
1562                 #print("\nShift released")  # debug
1563
1564         if event.type == 'RIGHTMOUSE' and event.value == 'PRESS':
1565             if self.lmb_held:
1566                 bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW')
1567                 exit_addon(self)
1568                 return {'CANCELLED'}
1569             else:
1570                 return {'PASS_THROUGH'}
1571
1572         if event.type == 'LEFTMOUSE' and event.value == 'PRESS':
1573             self.lmb_held = True
1574
1575         elif event.type == 'UP_ARROW' and event.value == 'RELEASE':
1576             if self.meas_btn.is_drawn:
1577                 self.menu.update_active(-1)
1578
1579         elif event.type == 'DOWN_ARROW' and event.value == 'RELEASE':
1580             if self.meas_btn.is_drawn:
1581                 self.menu.update_active( 1)
1582
1583         elif event.type in {'RET', 'LEFTMOUSE'} and event.value == 'RELEASE':
1584             # prevent click/enter that launched add-on from doing anything
1585             if self.first_run:
1586                 self.first_run = False
1587                 return {'RUNNING_MODAL'}
1588             if event.type == 'LEFTMOUSE':
1589                 self.lmb_held = False
1590             #print("LeftMouse released")  # debug
1591             self.mouse_co = Vector((event.mouse_region_x, event.mouse_region_y))
1592
1593             #===========================
1594             # Check for 0 or 180 click
1595             #===========================
1596             if self.addon_mode == GET_0_OR_180:
1597                 self.new_free_co, RotDat.ang_diff_r = choose_0_or_180(
1598                         self.pts[2], RotDat.rot_pt_pos, RotDat.rot_pt_neg,
1599                         RotDat.ang_diff_r, self.mouse_co
1600                 )
1601                 self.addon_mode = DO_TRANSFORM  # todo : find why this needed
1602                 do_transform(self)
1603
1604             #===================================
1605             # Check for click on Measure Button
1606             #===================================
1607             elif self.meas_btn.is_drawn and self.meas_btn.ms_over:
1608                 #print("\nMeas Button Clicked")
1609                 if can_transf(self):
1610                     global popup_active
1611                     self.addon_mode = WAIT_FOR_POPUP
1612                     popup_active = True
1613                     set_help_text(self, "POPUP")
1614                     bpy.ops.object.ms_input_dialog_op('INVOKE_DEFAULT')
1615
1616             #===========================================
1617             # Check for click on "Add Selected" Button
1618             #===========================================
1619             elif self.add_rm_btn.ms_over:
1620                 if self.mod_pt is not None:
1621                     if not self.shift_held:
1622                         add_select_multi(self)
1623                     else:
1624                         if self.pt_cnt < 3:
1625                             new_select_multi(self)
1626                             exit_multi_mode(self)
1627                             self.menu.change_menu(self.pt_cnt)
1628                 elif self.grab_pt is not None:
1629                     co3d = None
1630                     if bpy.context.mode == "OBJECT":
1631                         if len(bpy.context.selected_objects) > 0:
1632                             if not self.shift_held:
1633                                 co3d = bpy.context.selected_objects[0].location
1634                             else:
1635                                 new_select_multi(self)
1636                                 exit_multi_mode(self)
1637                                 self.menu.change_menu(self.pt_cnt)
1638                     elif bpy.context.mode == "EDIT_MESH":
1639                         m_w = bpy.context.edit_object.matrix_world
1640                         bm = bmesh.from_edit_mesh(bpy.context.edit_object.data)
1641                         if len(bm.select_history) > 0:
1642                             if not self.shift_held:
1643                                 for sel in bm.select_history:
1644                                     if type(sel) is bmesh.types.BMVert:
1645                                         co3d = m_w * sel.co
1646                                         break
1647                                     elif type(sel) is bmesh.types.BMEdge or \
1648                                             type(sel) is bmesh.types.BMFace:
1649                                         co3d = Vector()
1650                                         for v in sel.verts:
1651                                             co3d += m_w * v.co
1652                                         co3d = co3d / len(sel.verts)
1653                                         break
1654                             else:
1655                                 new_select_multi(self)
1656                                 exit_multi_mode(self)
1657                                 self.menu.change_menu(self.pt_cnt)
1658
1659                     if co3d is not None:
1660                         if not in_ref_pts(self, co3d):
1661                             self.pts[self.grab_pt].co3d = co3d
1662                         else:
1663                             swap_ref_pts(self, self.grab_pt, self.swap_pt)
1664                             self.swap_pt = None
1665                     self.grab_pt = None
1666                     updatelock_pts(self, self.pts)
1667                     set_meas_btn(self)
1668                 else:  # no grab or mod point
1669                     if self.shift_held:
1670                         if self.pt_cnt < 3:
1671                             new_select_multi(self)
1672                             if in_ref_pts(self, self.multi_tmp.get_co(), self.mod_pt):
1673                                 self.report({'WARNING'}, 'Points overlap.')
1674                             self.pts[self.mod_pt].co3d = self.multi_tmp.get_co()
1675                             self.menu.change_menu(self.pt_cnt)
1676                     else:
1677                         add_select(self)
1678                 # todo : see if this is really a good solution...
1679                 if self.mod_pt is None:
1680                     set_help_text(self, "CLICK")
1681                 else:
1682                     set_help_text(self, "MULTI")
1683
1684             #===========================
1685             # Point Place or Grab Mode
1686             #===========================
1687             elif self.mod_pt is None:
1688                 if self.overlap_idx is None:  # no point overlap
1689                     if not self.shift_held:
1690                         if self.grab_pt is not None:
1691                             found_pt = find_closest_point(self.mouse_co)
1692                             if found_pt is not None:
1693                                 if not in_ref_pts(self, found_pt):
1694                                     self.pts[self.grab_pt].co3d = found_pt
1695                             self.grab_pt = None
1696                             if self.pt_cnt > 1:
1697                                 updatelock_pts(self, self.pts)
1698                             set_mouse_highlight(self)
1699                             set_meas_btn(self)
1700                             set_help_text(self, "CLICK")
1701                         elif self.pt_cnt < 3:
1702                             found_pt = find_closest_point(self.mouse_co)
1703                             if found_pt is not None:
1704                                 if not in_ref_pts(self, found_pt):
1705                                     self.pts[self.pt_cnt].co3d = found_pt
1706                                     self.pt_cnt += 1
1707                                     self.menu.change_menu(self.pt_cnt)
1708                                     if self.pt_cnt > 1:
1709                                         updatelock_pts(self, self.pts)
1710                                         #if self.pt_cnt
1711                                     set_mouse_highlight(self)
1712                                     set_meas_btn(self)
1713                                     set_help_text(self, "CLICK")
1714                                     ''' Begin Debug
1715                                     cnt = self.pt_cnt - 1
1716                                     pt_fnd_str = str(self.pts[cnt].co3d)
1717                                     pt_fnd_str = pt_fnd_str.replace("<Vector ", "Vector(")
1718                                     pt_fnd_str = pt_fnd_str.replace(">", ")")
1719                                     print("ref_pt_" + str(cnt) + ' =', pt_fnd_str)
1720                                     #print("ref pt added:", self.cnt, "cnt:", self.cnt+1)
1721                                     End Debug '''
1722                 else:  # overlap
1723                     if self.grab_pt is not None:
1724                         if not self.shift_held:
1725                             if self.grab_pt != self.overlap_idx:
1726                                 swap_ref_pts(self, self.grab_pt, self.overlap_idx)
1727                                 set_meas_btn(self)
1728                             self.grab_pt = None
1729                             if self.pt_cnt > 1:
1730                                 updatelock_pts(self, self.pts)
1731                             set_mouse_highlight(self)
1732                             set_meas_btn(self)
1733                             set_help_text(self, "CLICK")
1734
1735                     elif not self.shift_held:
1736                         # overlap and shift not held == remove point
1737                         rem_ref_pt(self, self.overlap_idx)
1738                         set_meas_btn(self)
1739                         set_help_text(self, "CLICK")
1740                     else:  # shift_held
1741                         # enable multi point mode
1742                         self.mod_pt = self.overlap_idx
1743                         self.multi_tmp.reset(self.pts[self.mod_pt].co3d)
1744                         self.highlight_mouse = True
1745                         set_help_text(self, "MULTI")
1746
1747             #===========================
1748             # Mod Ref Point Mode
1749             #===========================
1750             else:  # mod_pt exists
1751                 if self.overlap_idx is None:  # no point overlap
1752                     if not self.shift_held:
1753                         # attempt to add new point to multi_tmp
1754                         found_pt = find_closest_point(self.mouse_co)
1755                         if found_pt is not None:
1756                             self.multi_tmp.try_add(found_pt)
1757                             mult_co3d = self.multi_tmp.get_co()
1758                             if in_ref_pts(self, mult_co3d, self.mod_pt):
1759                                 self.report({'WARNING'}, 'Points overlap.')
1760                             self.pts[self.mod_pt].co3d = mult_co3d
1761                     else:  # shift_held, exit multi_tmp
1762                         exit_multi_mode(self)
1763                 else:  # overlap multi_tmp
1764                     if not self.shift_held:
1765                         # remove multi_tmp point
1766                         self.multi_tmp.rem_pt(self.overlap_idx)
1767                         # if all multi_tmp points removed,
1768                         # exit multi mode, remove edited point
1769                         if self.multi_tmp.co3d is None:
1770                             rem_ref_pt(self, self.mod_pt)
1771                             self.mod_pt = None
1772                             set_meas_btn(self)
1773                             set_help_text(self, "CLICK")
1774                         elif in_ref_pts(self, self.multi_tmp.co3d, self.mod_pt):
1775                             self.report({'WARNING'}, 'Points overlap.')
1776                             self.pts[self.mod_pt].co3d = self.multi_tmp.get_co()
1777                         else:
1778                             self.pts[self.mod_pt].co3d = self.multi_tmp.get_co()
1779                     else:  # shift_held
1780                         exit_multi_mode(self)
1781
1782         if event.type == 'C' and event.value == 'PRESS':
1783             #print("Pressed C\n")  # debug
1784             axis_key_check(self, None)
1785
1786         elif event.type == 'X' and event.value == 'PRESS':
1787             #print("Pressed X\n")  # debug
1788             axis_key_check(self, 'X')
1789
1790         elif event.type == 'Y' and event.value == 'PRESS':
1791             #print("Pressed Y\n")  # debug
1792             axis_key_check(self, 'Y')
1793
1794         elif event.type == 'Z' and event.value == 'PRESS':
1795             #print("Pressed Z\n")  # debug
1796             axis_key_check(self, 'Z')
1797
1798             '''
1799             elif event.type == 'D' and event.value == 'RELEASE':
1800                 # open debug console
1801                 __import__('code').interact(local=dict(globals(), **locals()))
1802             '''
1803
1804         elif event.type == 'G' and event.value == 'RELEASE':
1805             # if already in grab mode, cancel grab
1806             if self.grab_pt is not None:
1807                 self.grab_pt = None
1808                 set_mouse_highlight(self)
1809                 set_help_text(self, "CLICK")
1810             # else enable grab mode (if possible)
1811             elif self.mod_pt is None:
1812                 if self.overlap_idx is not None:
1813                     self.grab_pt = self.overlap_idx
1814                     self.highlight_mouse = False
1815                     set_help_text(self, "GRAB")
1816
1817         elif event.type in {'ESC'} and event.value == 'RELEASE':
1818             bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW')
1819             exit_addon(self)
1820             return {'CANCELLED'}
1821
1822         if self.force_quit:
1823             bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW')
1824             exit_addon(self)
1825             return {'FINISHED'}
1826
1827         return {'RUNNING_MODAL'}
1828
1829     def invoke(self, context, event):
1830         if context.area.type == 'VIEW_3D':
1831             args = (self, context)
1832
1833             # Add the region OpenGL drawing callback
1834             # draw in view space with 'POST_VIEW' and 'PRE_VIEW'
1835             self._handle = bpy.types.SpaceView3D.draw_handler_add(draw_callback_px,
1836                     args, 'WINDOW', 'POST_PIXEL')
1837
1838             self.settings_backup = backup_blender_settings()
1839             self.mouse_co = Vector((event.mouse_region_x, event.mouse_region_y))
1840             self.rtoolsw = get_reg_overlap()  # region tools (toolbar) width
1841             self.highlight_mouse = True  # draw ref point on mouse
1842             self.pts = []
1843             self.pt_cnt = 0
1844             self.lk_pts = []
1845             self.multi_tmp = TempPoint()
1846             self.meas_btn = ViewButton(Colr.red, Colr.white, 18, Colr.white, (0, 20))
1847             self.add_rm_btn = ViewButton(Colr.red, Colr.white, 18, Colr.white, (190, 36))
1848             self.overlap_idx = None
1849             self.shift_held = False
1850             #self.debug_flag = False
1851             self.mod_pt = None
1852             self.first_run = event.type in {'RET', 'LEFTMOUSE'} and event.value != 'RELEASE'
1853             self.force_quit = False
1854             self.grab_pt = None
1855             self.new_free_co = ()
1856             self.swap_pt = None
1857             self.addon_mode = CLICK_CHECK
1858             self.transf_type = ""  # transform type
1859             #self.pt_find_md = SLOW3DTO2D  # point find mode
1860             self.lmb_held = False
1861
1862             self.menu = MenuHandler("Set Measaure", 18, Colr.yellow, Colr.white, \
1863                     self.rtoolsw, context.region)
1864             self.menu.add_menu(["Move", "Scale"])
1865             self.menu.add_menu(["Rotate"])
1866
1867             context.window_manager.modal_handler_add(self)
1868
1869             init_ref_pts(self)
1870             init_blender_settings()
1871             editmode_refresh()
1872             #print("Add-on started")  # debug
1873             self.add_rm_btn.set_text("Add Selected")
1874             set_help_text(self, "CLICK")
1875
1876             return {'RUNNING_MODAL'}
1877         else:
1878             self.report({'WARNING'}, "View3D not found, cannot run operator")
1879             return {'CANCELLED'}