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