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.
21 from copy
import deepcopy
22 from math
import degrees
, radians
, pi
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
54 popup_meas_backup
= 0.0
58 prev_popup_inputs
= []
59 prev_popup_inp_strings
= []
61 #print("Loaded add-on.\n") # debug
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
77 values stored here get used for translation, scale, and rotation
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():
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.
100 if bpy
.context
.mode
== "EDIT_MESH":
101 bpy
.ops
.object.editmode_toggle()
102 bpy
.ops
.object.editmode_toggle()
105 def backup_blender_settings():
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
)]
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
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])
138 def flts_alm_eq(flt_a
, flt_b
):
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
)):
151 # todo : replace with flt_lists_alm_eq?
152 def vec3s_alm_eq(vec_a
, vec_b
):
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
]):
164 self
.active
= 0 # unused ?
165 # todo : replace above with self.current ?
169 self
.arrows
= [] # arrow coordinates
173 def __init__(self
, title
, tsize
, act_colr
, dis_colr
, toolwid
, reg
):
174 self
.dpi
= bpy
.context
.preferences
.system
.dpi
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
186 self
.view_offset
= 20, 95 # box left top start
187 self
.box_y_pad
= 8 # vertical space between boxes
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
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())
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
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
228 def update_active(self
, change
):
229 menu
= self
.menus
[self
.current
]
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
):
240 menu
= self
.menus
[self
.current
]
241 return menu
.texts
[menu
.active
]
243 #def rebuild_menus(self) # add in case blender window size changes?
246 def draw(self
, menu_visible
):
247 menu
= self
.menus
[self
.current
]
248 # prepare to draw text
250 blf
.size(font_id
, self
.tsize
, self
.dpi
)
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
)
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
])
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]:
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
)
275 shader
.uniform_float("color", self
.act_colr
)
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
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
)
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")
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
,
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
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
):
345 #print("Cancelled Pop-Up") # debug
348 def check(self
, context
):
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
])
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 ===
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
381 #self.co_outside_btn = None
383 self
.colr_off
= colr_off
# colr when mouse not over button
384 self
.colr_on
= colr_on
# colr when mouse over button
387 self
.txt_colr
= txt_colr
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.
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
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
):
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)
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]:
434 if my
< self
.ms_chk
[2] or my
> self
.ms_chk
[3]:
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
443 self
.set_btn_coor(offs_loc
)
444 if self
.pt_inside_btn2(mouse_co
):
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])
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
)
462 shader
.uniform_float("color", colr
)
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)
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
})
479 shader
.uniform_float("color", self
.colr_off
)
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
)
492 # Used for mod_pt mode
495 self
.ls
= [] # point list
504 self
.co3d
= vsum
/ self
.cnt
506 def find_pt(self
, co3d
):
508 for i
in range(self
.cnt
):
509 if self
.ls
[i
] == co3d
:
514 def rem_pt(self
, idx
):
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())
530 def reset(self
, co3d
):
531 self
.co3d
= co3d
.copy()
532 self
.ls
= [co3d
.copy()]
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
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)
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
)
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
):
580 ReferencePoint("fre", Colr
.green
),
581 ReferencePoint("anc", Colr
.red
),
582 ReferencePoint("piv", Colr
.yellow
)
586 def set_mouse_highlight(self
):
588 self
.highlight_mouse
= True
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
)
600 if vec3s_alm_eq(self
.pts
[i
].co3d
, co3d
):
602 self
.swap_pt
= i
# todo : better solution than this
607 def add_pt(self
, co3d
):
608 if not in_ref_pts(self
, co3d
):
609 self
.pts
[self
.pt_cnt
].co3d
= co3d
611 self
.menu
.change_menu(self
.pt_cnt
)
613 update_lock_pts(self
, self
.pts
)
614 set_mouse_highlight(self
)
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)
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
]
631 for i
in range(len(keep_idx
)):
632 self
.pts
[i
].co3d
= self
.pts
[keep_idx
[i
]].co3d
.copy()
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
639 update_lock_pts(self
, self
.pts
)
641 TransDat
.axis_lock
= None
642 self
.highlight_mouse
= True
645 def add_select(self
):
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())
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
:
660 if type(sel
) is bmesh
.types
.BMVert
:
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
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
:
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
:
692 if type(sel
) is bmesh
.types
.BMVert
:
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
700 self
.multi_tmp
.try_add(v_co3d
)
701 if self
.multi_tmp
.cnt
== self
.multi_tmp
.max_cnt
:
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
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'))
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
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
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
:
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
:
766 if type(sel
) is bmesh
.types
.BMVert
:
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
774 self
.multi_tmp
.try_add(v_co3d
)
775 if self
.multi_tmp
.cnt
== self
.multi_tmp
.max_cnt
:
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
)
790 self
.pts
[self
.mod_pt
].co3d
= m_co3d
792 update_lock_pts(self
, self
.pts
)
793 set_mouse_highlight(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
809 for obj
in bpy
.context
.scene
.objects
:
810 o_co2d
= loc3d_to_reg2d(region
, rv3d
, obj
.location
)
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
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)
841 def draw_pt_2d(pt_co
, pt_color
, pt_size
):
842 if pt_co
is not None:
844 bgl
.glPointSize(pt_size
)
845 shader
= gpu
.shader
.from_builtin('2D_UNIFORM_COLOR')
846 batch
= batch_for_shader(shader
, 'POINTS', {"pos": coords
})
848 shader
.uniform_float("color", pt_color
)
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)
856 bgl.glColor4f(*pt_color)
857 bgl.glBegin(bgl.GL_LINE_STRIP)
858 bgl.glVertex2f(*pt_co_1)
859 bgl.glVertex2f(*pt_co_2)
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
})
870 shader
.uniform_float("color", pt_color
)
874 def closest_to_point(pt
, pts
):
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
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
897 mode
= self
.menu
.get_mode()
899 self
.transf_type
= MOVE
901 elif mode
== "Scale":
902 self
.transf_type
= SCALE
905 elif self
.pt_cnt
== 3:
906 self
.transf_type
= ROTATE
907 if TransDat
.axis_lock
is not None:
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
)
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.")
924 def slope_check(pt1
, pt2
):
925 '''For making sure rise over run doesn't get flipped.'''
927 for i
in range(len(pt1
)):
928 cmp_ls
.append(flts_alm_eq(pt1
[i
], pt2
[i
]) or pt1
[i
] > pt2
[i
])
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:
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
:
952 # for negative distances
954 elif orig_slope
== pt_neg_slp
:
959 else: # neither slope matches
960 self
.report({'ERROR'}, 'Slope mismatch. Cannot calculate new point.')
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] ])
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] ])
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
])
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.")
983 def set_arc_pts(ref_pts
):
984 fre
, anc
, piv
= ref_pts
[0].co3d
, ref_pts
[1].co3d
, ref_pts
[2].co3d
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
)
992 rotated
.rotate(rot_val
)
994 rot_ang
= (anc
- piv
).angle(rotated
- piv
)
995 if not flts_alm_eq(rot_ang
, 0.0):
997 dis_p_f
= (piv
- fre
).length
998 dis_p_a
= (piv
- anc
).length
999 if dis_p_f
< dis_p_a
:
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
:
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
]
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 ?
1059 TransDat
.lock_pts
= []
1060 elif TransDat
.axis_lock
is None:
1061 TransDat
.lock_pts
= ref_pts
1063 set_arc_pts(ref_pts
)
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
])
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
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 ?
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
1255 draw_pt_2d(rot_pos_co2d
, Colr
.grey
, psize_small
)
1256 draw_pt_2d(rot_neg_co2d
, Colr
.grey
, psize_small
)
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))
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)):
1289 new_x
= new_y
.cross(new_z
)
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
,
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
)
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
)
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))
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
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.
1406 if new_axis
!= TransDat
.axis_lock
:
1407 TransDat
.axis_lock
= new_axis
1408 update_lock_pts(self
, self
.pts
)
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))
1421 self
.meas_btn
.is_drawn
= False
1422 set_lock_pts(self
.pts
, self
.pt_cnt
)
1424 update_lock_pts(self
, self
.pts
)
1425 self
.meas_btn
.is_drawn
= True
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.')
1439 self
.menu
.change_menu(self
.pt_cnt
)
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
:
1496 elif self
.transf_type
== SCALE
:
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
)
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
)
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
)
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
)
1533 def set_help_text(self
, mode
):
1534 '''Called when add-on mode changes and every time point is added or removed.'''
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
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
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
)
1597 draw_pt_2d(self
.mouse_co
, Colr
.black
, ptsz_lrg
)
1598 if len(m_pts2d
) > 1:
1600 draw_pt_2d(mp
, Colr
.black
, ptsz_lrg
)
1602 draw_pt_2d(closest_pt
, Colr
.black
, ptsz_lrg
)
1603 if len(m_pts2d
) > 1:
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
)
1613 draw_pt_2d(closest_pt
, Colr
.white
, ptsz_lrg
)
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
]
1619 # draw axis lock indicator
1620 if TransDat
.axis_lock
== 'X':
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
)
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?
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':
1683 #elif r.type == 'UI':
1685 #return rtoolsw, ruiw
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
1695 def poll(self
, context
):
1696 return context
.mode
== 'OBJECT' or context
.mode
== 'EDIT_MESH'
1698 def modal(self
, context
, event
):
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':
1721 bpy
.types
.SpaceView3D
.draw_handler_remove(self
._handle
, 'WINDOW')
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
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
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
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
)
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:
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
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
:
1801 elif type(sel
) is bmesh
.types
.BMEdge
or \
1802 type(sel
) is bmesh
.types
.BMFace
:
1806 co3d
= co3d
/ len(sel
.verts
)
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
1817 swap_ref_pts(self
, self
.grab_pt
, self
.swap_pt
)
1820 update_lock_pts(self
, self
.pts
)
1822 else: # no grab or mod point
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
)
1832 # todo : see if this is really a good solution...
1833 if self
.mod_pt
is None:
1834 set_help_text(self
, "CLICK")
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
1851 update_lock_pts(self
, self
.pts
)
1852 set_mouse_highlight(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
1861 self
.menu
.change_menu(self
.pt_cnt
)
1863 update_lock_pts(self
, self
.pts
)
1865 set_mouse_highlight(self
)
1867 set_help_text(self
, "CLICK")
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)
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
)
1884 update_lock_pts(self
, self
.pts
)
1885 set_mouse_highlight(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
)
1893 set_help_text(self
, "CLICK")
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
)
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()
1932 self
.pts
[self
.mod_pt
].co3d
= self
.multi_tmp
.get_co()
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:
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')
1974 return {'CANCELLED'}
1977 bpy
.types
.SpaceView3D
.draw_handler_remove(self
._handle
, 'WINDOW')
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
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
2015 self
.first_run
= event
.type in {'RET', 'LEFTMOUSE'} and event
.value
!= 'RELEASE'
2016 self
.force_quit
= False
2018 self
.new_free_co
= ()
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()
2034 set_transform_data_none()
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'}
2042 self
.report({'WARNING'}, "View3D not found, cannot run operator")
2043 return {'CANCELLED'}