Fix invalid string comparisons
[blender-addons.git] / mesh_snap_utilities_line.py
blob8f2f2a94546ea53d88564a75f678de7a1be3de0d
1 # ##### BEGIN GPL LICENSE BLOCK #####
3 # This program is free software; you can redistribute it and/or
4 # modify it under the terms of the GNU General Public License
5 # as published by the Free Software Foundation; either version 3
6 # of the License, or (at your option) any later version.
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
13 # You should have received a copy of the GNU General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 # ##### END GPL LICENSE BLOCK #####
18 # Contact for more information about the Addon:
19 # Email: germano.costa@ig.com.br
20 # Twitter: wii_mano @mano_wii
23 bl_info = {
24 "name": "Snap Utilities Line",
25 "author": "Germano Cavalcante",
26 "version": (5, 7, 6),
27 "blender": (2, 75, 0),
28 "location": "View3D > TOOLS > Snap Utilities > snap utilities",
29 "description": "Extends Blender Snap controls",
30 "wiki_url": "https://wiki.blender.org/index.php/Extensions:2.6/Py/Scripts/Modeling/Snap_Utils_Line",
31 "category": "Mesh"}
33 import bpy
34 import bgl
35 import bmesh
36 from mathutils import Vector
37 from mathutils.geometry import (
38 intersect_point_line,
39 intersect_line_line,
40 intersect_line_plane,
41 intersect_ray_tri
43 from bpy.types import (
44 Operator,
45 Panel,
46 AddonPreferences,
48 from bpy.props import (
49 BoolProperty,
50 FloatProperty,
51 FloatVectorProperty,
52 StringProperty,
55 ##DEBUG = False
56 ##if DEBUG:
57 ## from .snap_framebuffer_debug import screenTexture
58 ## from .snap_context import mesh_drawing
61 def get_units_info(scale, unit_system, separate_units):
62 if unit_system == 'METRIC':
63 scale_steps = ((1000, 'km'), (1, 'm'), (1 / 100, 'cm'),
64 (1 / 1000, 'mm'), (1 / 1000000, '\u00b5m'))
65 elif unit_system == 'IMPERIAL':
66 scale_steps = ((5280, 'mi'), (1, '\''),
67 (1 / 12, '"'), (1 / 12000, 'thou'))
68 scale /= 0.3048 # BU to feet
69 else:
70 scale_steps = ((1, ' BU'),)
71 separate_units = False
73 return (scale, scale_steps, separate_units)
76 def convert_distance(val, units_info, precision=5):
77 scale, scale_steps, separate_units = units_info
78 sval = val * scale
79 idx = 0
80 while idx < len(scale_steps) - 1:
81 if sval >= scale_steps[idx][0]:
82 break
83 idx += 1
84 factor, suffix = scale_steps[idx]
85 sval /= factor
86 if not separate_units or idx == len(scale_steps) - 1:
87 dval = str(round(sval, precision)) + suffix
88 else:
89 ival = int(sval)
90 dval = str(round(ival, precision)) + suffix
91 fval = sval - ival
92 idx += 1
93 while idx < len(scale_steps):
94 fval *= scale_steps[idx - 1][0] / scale_steps[idx][0]
95 if fval >= 1:
96 dval += ' ' \
97 + ("%.1f" % fval) \
98 + scale_steps[idx][1]
99 break
100 idx += 1
102 return dval
105 def location_3d_to_region_2d(region, rv3d, coord):
106 prj = rv3d.perspective_matrix * Vector((coord[0], coord[1], coord[2], 1.0))
107 width_half = region.width / 2.0
108 height_half = region.height / 2.0
109 return Vector((width_half + width_half * (prj.x / prj.w),
110 height_half + height_half * (prj.y / prj.w),
111 prj.z / prj.w
115 def out_Location(rv3d, region, orig, vector):
116 view_matrix = rv3d.view_matrix
117 v1 = Vector((int(view_matrix[0][0] * 1.5), int(view_matrix[0][1] * 1.5), int(view_matrix[0][2] * 1.5)))
118 v2 = Vector((int(view_matrix[1][0] * 1.5), int(view_matrix[1][1] * 1.5), int(view_matrix[1][2] * 1.5)))
120 hit = intersect_ray_tri(Vector((1, 0, 0)), Vector((0, 1, 0)), Vector(), (vector), (orig), False)
121 if hit is None:
122 hit = intersect_ray_tri(v1, v2, Vector(), (vector), (orig), False)
123 if hit is None:
124 hit = intersect_ray_tri(v1, v2, Vector(), (-vector), (orig), False)
125 if hit is None:
126 hit = Vector()
127 return hit
130 def get_closest_edge(bm, point, dist):
131 r_edge = None
132 for edge in bm.edges:
133 v1 = edge.verts[0].co
134 v2 = edge.verts[1].co
135 # Test the BVH (AABB) first
136 for i in range(3):
137 if v1[i] <= v2[i]:
138 isect = v1[i] - dist <= point[i] <= v2[i] + dist
139 else:
140 isect = v2[i] - dist <= point[i] <= v1[i] + dist
142 if not isect:
143 break
144 else:
145 ret = intersect_point_line(point, v1, v2)
147 if ret[1] < 0.0:
148 tmp = v1
149 elif ret[1] > 1.0:
150 tmp = v2
151 else:
152 tmp = ret[0]
154 new_dist = (point - tmp).length
155 if new_dist <= dist:
156 dist = new_dist
157 r_edge = edge
159 return r_edge
162 class SnapCache():
163 bvert = None
164 vco = None
166 bedge = None
167 v0 = None
168 v1 = None
169 vmid = None
170 vperp = None
171 v2d0 = None
172 v2d1 = None
173 v2dmid = None
174 v2dperp = None
176 bm_geom_selected = None
179 def snap_utilities(
180 sctx, obj,
181 cache, context, obj_matrix_world,
182 bm, mcursor,
183 constrain = None,
184 previous_vert = None,
185 increment = 0.0):
187 rv3d = context.region_data
188 region = context.region
189 scene = context.scene
190 is_increment = False
191 r_loc = None
192 r_type = None
193 r_len = 0.0
194 bm_geom = None
196 if cache.bm_geom_selected:
197 try:
198 cache.bm_geom_selected.select = False
199 except ReferenceError as e:
200 print(e)
202 snp_obj, loc, elem = sctx.snap_get(mcursor)
203 view_vector, orig = sctx.last_ray
205 if not snp_obj:
206 is_increment = True
207 if constrain:
208 end = orig + view_vector
209 t_loc = intersect_line_line(constrain[0], constrain[1], orig, end)
210 if t_loc is None:
211 t_loc = constrain
212 r_loc = t_loc[0]
213 else:
214 r_type = 'OUT'
215 r_loc = out_Location(rv3d, region, orig, view_vector)
217 elif snp_obj.data[0] != obj: #OUT
218 r_loc = loc
220 if constrain:
221 is_increment = False
222 r_loc = intersect_point_line(r_loc, constrain[0], constrain[1])[0]
223 if not r_loc:
224 r_loc = out_Location(rv3d, region, orig, view_vector)
225 elif len(elem) == 1:
226 is_increment = False
227 r_type = 'VERT'
228 elif len(elem) == 2:
229 is_increment = True
230 r_type = 'EDGE'
231 else:
232 is_increment = True
233 r_type = 'FACE'
235 elif len(elem) == 1:
236 r_type = 'VERT'
237 bm_geom = bm.verts[elem[0]]
239 if cache.bvert != bm_geom:
240 cache.bvert = bm_geom
241 cache.vco = loc
242 #cache.v2d = location_3d_to_region_2d(region, rv3d, cache.vco)
244 if constrain:
245 r_loc = intersect_point_line(cache.vco, constrain[0], constrain[1])[0]
246 else:
247 r_loc = cache.vco
249 elif len(elem) == 2:
250 v1 = bm.verts[elem[0]]
251 v2 = bm.verts[elem[1]]
252 bm_geom = bm.edges.get([v1, v2])
254 if cache.bedge != bm_geom:
255 cache.bedge = bm_geom
256 cache.v0 = obj_matrix_world * v1.co
257 cache.v1 = obj_matrix_world * v2.co
258 cache.vmid = 0.5 * (cache.v0 + cache.v1)
259 cache.v2d0 = location_3d_to_region_2d(region, rv3d, cache.v0)
260 cache.v2d1 = location_3d_to_region_2d(region, rv3d, cache.v1)
261 cache.v2dmid = location_3d_to_region_2d(region, rv3d, cache.vmid)
263 if previous_vert and previous_vert not in {v1, v2}:
264 pvert_co = obj_matrix_world * previous_vert.co
265 perp_point = intersect_point_line(pvert_co, cache.v0, cache.v1)
266 cache.vperp = perp_point[0]
267 #factor = point_perpendicular[1]
268 cache.v2dperp = location_3d_to_region_2d(region, rv3d, perp_point[0])
270 #else: cache.v2dperp = None
272 if constrain:
273 t_loc = intersect_line_line(constrain[0], constrain[1], cache.v0, cache.v1)
274 if t_loc is None:
275 is_increment = True
276 end = orig + view_vector
277 t_loc = intersect_line_line(constrain[0], constrain[1], orig, end)
278 r_loc = t_loc[0]
280 elif cache.v2dperp and\
281 abs(cache.v2dperp[0] - mcursor[0]) < 10 and abs(cache.v2dperp[1] - mcursor[1]) < 10:
282 r_type = 'PERPENDICULAR'
283 r_loc = cache.vperp
285 elif abs(cache.v2dmid[0] - mcursor[0]) < 10 and abs(cache.v2dmid[1] - mcursor[1]) < 10:
286 r_type = 'CENTER'
287 r_loc = cache.vmid
289 else:
290 if increment and previous_vert in cache.bedge.verts:
291 is_increment = True
293 r_type = 'EDGE'
294 r_loc = loc
296 elif len(elem) == 3:
297 is_increment = True
298 r_type = 'FACE'
299 tri = [
300 bm.verts[elem[0]],
301 bm.verts[elem[1]],
302 bm.verts[elem[2]],
305 faces = set(tri[0].link_faces).intersection(tri[1].link_faces, tri[2].link_faces)
306 if len(faces) == 1:
307 bm_geom = faces.pop()
308 else:
309 i = -2
310 edge = None
311 while not edge and i != 1:
312 edge = bm.edges.get([tri[i], tri[i + 1]])
313 i += 1
314 if edge:
315 for l in edge.link_loops:
316 if l.link_loop_next.vert == tri[i] or l.link_loop_prev.vert == tri[i - 2]:
317 bm_geom = l.face
318 break
319 else: # This should never happen!!!!
320 raise
321 bm_geom = faces.pop()
323 r_loc = loc
325 if constrain:
326 is_increment = False
327 r_loc = intersect_point_line(r_loc, constrain[0], constrain[1])[0]
329 if previous_vert:
330 pv_co = obj_matrix_world * previous_vert.co
331 vec = r_loc - pv_co
332 if is_increment and increment:
333 r_len = round((1 / increment) * vec.length) * increment
334 r_loc = r_len * vec.normalized() + pv_co
335 else:
336 r_len = vec.length
338 if bm_geom:
339 bm_geom.select = True
341 cache.bm_geom_selected = bm_geom
343 return r_loc, r_type, bm_geom, r_len
346 def get_loose_linked_edges(bmvert):
347 linked = [e for e in bmvert.link_edges if not e.link_faces]
348 for e in linked:
349 linked += [le for v in e.verts if not v.link_faces for le in v.link_edges if le not in linked]
350 return linked
353 def draw_line(self, obj, bm, bm_geom, location):
354 split_faces = set()
356 drawing_is_dirt = False
357 update_edit_mesh = False
358 tessface = False
360 if bm_geom is None:
361 vert = bm.verts.new(location)
362 self.list_verts.append(vert)
364 elif isinstance(bm_geom, bmesh.types.BMVert):
365 if (bm_geom.co - location).length_squared < .001:
366 if self.list_verts == [] or self.list_verts[-1] != bm_geom:
367 self.list_verts.append(bm_geom)
368 else:
369 vert = bm.verts.new(location)
370 self.list_verts.append(vert)
371 drawing_is_dirt = True
373 elif isinstance(bm_geom, bmesh.types.BMEdge):
374 self.list_edges.append(bm_geom)
375 ret = intersect_point_line(location, bm_geom.verts[0].co, bm_geom.verts[1].co)
377 if (ret[0] - location).length_squared < .001:
378 if ret[1] == 0.0:
379 vert = bm_geom.verts[0]
380 elif ret[1] == 1.0:
381 vert = bm_geom.verts[1]
382 else:
383 edge, vert = bmesh.utils.edge_split(bm_geom, bm_geom.verts[0], ret[1])
384 drawing_is_dirt = True
385 self.list_verts.append(vert)
386 # self.list_edges.append(edge)
388 else: # constrain point is near
389 vert = bm.verts.new(location)
390 self.list_verts.append(vert)
391 drawing_is_dirt = True
393 elif isinstance(bm_geom, bmesh.types.BMFace):
394 split_faces.add(bm_geom)
395 vert = bm.verts.new(location)
396 self.list_verts.append(vert)
397 drawing_is_dirt = True
399 # draw, split and create face
400 if len(self.list_verts) >= 2:
401 v1, v2 = self.list_verts[-2:]
402 # v2_link_verts = [x for y in [a.verts for a in v2.link_edges] for x in y if x != v2]
403 edge = bm.edges.get([v1, v2])
404 if edge:
405 self.list_edges.append(edge)
407 else: # if v1 not in v2_link_verts:
408 if not v2.link_edges:
409 edge = bm.edges.new([v1, v2])
410 self.list_edges.append(edge)
411 drawing_is_dirt = True
412 else: # split face
413 v1_link_faces = v1.link_faces
414 v2_link_faces = v2.link_faces
415 if v1_link_faces and v2_link_faces:
416 split_faces.update(set(v1_link_faces).intersection(v2_link_faces))
418 else:
419 if v1_link_faces:
420 faces = v1_link_faces
421 co2 = v2.co.copy()
422 else:
423 faces = v2_link_faces
424 co2 = v1.co.copy()
426 for face in faces:
427 if bmesh.geometry.intersect_face_point(face, co2):
428 co = co2 - face.calc_center_median()
429 if co.dot(face.normal) < 0.001:
430 split_faces.add(face)
432 if split_faces:
433 edge = bm.edges.new([v1, v2])
434 self.list_edges.append(edge)
435 ed_list = get_loose_linked_edges(v2)
436 for face in split_faces:
437 facesp = bmesh.utils.face_split_edgenet(face, ed_list)
438 del split_faces
439 update_edit_mesh = True
440 tessface = True
441 else:
442 if self.intersect:
443 facesp = bmesh.ops.connect_vert_pair(bm, verts=[v1, v2], verts_exclude=bm.verts)
444 # print(facesp)
445 if not self.intersect or not facesp['edges']:
446 edge = bm.edges.new([v1, v2])
447 self.list_edges.append(edge)
448 drawing_is_dirt = True
449 else:
450 for edge in facesp['edges']:
451 self.list_edges.append(edge)
452 update_edit_mesh = True
453 tessface = True
455 # create face
456 if self.create_face:
457 ed_list = set(self.list_edges)
458 for edge in v2.link_edges:
459 for vert in edge.verts:
460 if vert != v2 and vert in self.list_verts:
461 ed_list.add(edge)
462 break
463 else:
464 continue
465 # Inner loop had a break, break the outer
466 break
468 ed_list.update(get_loose_linked_edges(v2))
470 bmesh.ops.edgenet_fill(bm, edges=list(ed_list))
471 update_edit_mesh = True
472 tessface = True
473 # print('face created')
474 if update_edit_mesh:
475 bmesh.update_edit_mesh(obj.data, tessface = tessface)
476 self.sctx.update_drawn_snap_object(self.snap_obj)
477 #bm.verts.index_update()
478 elif drawing_is_dirt:
479 self.obj.update_from_editmode()
480 self.sctx.update_drawn_snap_object(self.snap_obj)
482 return [obj.matrix_world * v.co for v in self.list_verts]
485 class NavigationKeys:
486 def __init__(self, context):
487 # TO DO:
488 # 'View Orbit', 'View Pan', 'NDOF Orbit View', 'NDOF Pan View'
489 self._rotate = set()
490 self._move = set()
491 self._zoom = set()
492 for key in context.window_manager.keyconfigs.user.keymaps['3D View'].keymap_items:
493 if key.idname == 'view3d.rotate':
494 #self.keys_rotate[key.id]={'Alt': key.alt, 'Ctrl': key.ctrl, 'Shift':key.shift, 'Type':key.type, 'Value':key.value}
495 self._rotate.add((key.alt, key.ctrl, key.shift, key.type, key.value))
496 if key.idname == 'view3d.move':
497 self._move.add((key.alt, key.ctrl, key.shift, key.type, key.value))
498 if key.idname == 'view3d.zoom':
499 if key.type == 'WHEELINMOUSE':
500 self._zoom.add((key.alt, key.ctrl, key.shift, 'WHEELUPMOUSE', key.value, key.properties.delta))
501 elif key.type == 'WHEELOUTMOUSE':
502 self._zoom.add((key.alt, key.ctrl, key.shift, 'WHEELDOWNMOUSE', key.value, key.properties.delta))
503 else:
504 self._zoom.add((key.alt, key.ctrl, key.shift, key.type, key.value, key.properties.delta))
507 class CharMap:
508 ascii = {
509 ".", ",", "-", "+", "1", "2", "3",
510 "4", "5", "6", "7", "8", "9", "0",
511 "c", "m", "d", "k", "h", "a",
512 " ", "/", "*", "'", "\""
513 # "="
515 type = {
516 'BACK_SPACE', 'DEL',
517 'LEFT_ARROW', 'RIGHT_ARROW'
520 @staticmethod
521 def modal(self, context, event):
522 c = event.ascii
523 if c:
524 if c == ",":
525 c = "."
526 self.length_entered = self.length_entered[:self.line_pos] + c + self.length_entered[self.line_pos:]
527 self.line_pos += 1
528 if self.length_entered:
529 if event.type == 'BACK_SPACE':
530 self.length_entered = self.length_entered[:self.line_pos - 1] + self.length_entered[self.line_pos:]
531 self.line_pos -= 1
533 elif event.type == 'DEL':
534 self.length_entered = self.length_entered[:self.line_pos] + self.length_entered[self.line_pos + 1:]
536 elif event.type == 'LEFT_ARROW':
537 self.line_pos = (self.line_pos - 1) % (len(self.length_entered) + 1)
539 elif event.type == 'RIGHT_ARROW':
540 self.line_pos = (self.line_pos + 1) % (len(self.length_entered) + 1)
543 class SnapUtilitiesLine(Operator):
544 bl_idname = "mesh.snap_utilities_line"
545 bl_label = "Line Tool"
546 bl_description = "Draw edges. Connect them to split faces"
547 bl_options = {'REGISTER', 'UNDO'}
549 constrain_keys = {
550 'X': Vector((1, 0, 0)),
551 'Y': Vector((0, 1, 0)),
552 'Z': Vector((0, 0, 1)),
553 'RIGHT_SHIFT': 'shift',
554 'LEFT_SHIFT': 'shift',
557 @classmethod
558 def poll(cls, context):
559 preferences = context.user_preferences.addons[__name__].preferences
560 return (context.mode in {'EDIT_MESH', 'OBJECT'} and
561 preferences.create_new_obj or
562 (context.object is not None and
563 context.object.type == 'MESH'))
566 def draw_callback_px(self, context):
567 # draw 3d point OpenGL in the 3D View
568 bgl.glEnable(bgl.GL_BLEND)
569 bgl.glDisable(bgl.GL_DEPTH_TEST)
570 # bgl.glPushMatrix()
571 # bgl.glMultMatrixf(self.obj_glmatrix)
573 ## if DEBUG:
574 ## mesh_drawing._store_current_shader_state(mesh_drawing.PreviousGLState)
575 ## self.screen.Draw(self.sctx._texture)
576 ## mesh_drawing._restore_shader_state(mesh_drawing.PreviousGLState)
578 if self.vector_constrain:
579 vc = self.vector_constrain
580 if hasattr(self, 'preloc') and self.type in {'VERT', 'FACE'}:
581 bgl.glColor4f(1.0, 1.0, 1.0, 0.5)
582 bgl.glPointSize(5)
583 bgl.glBegin(bgl.GL_POINTS)
584 bgl.glVertex3f(*self.preloc)
585 bgl.glEnd()
586 if vc[2] == 'X':
587 Color4f = (self.axis_x_color + (1.0,))
588 elif vc[2] == 'Y':
589 Color4f = (self.axis_y_color + (1.0,))
590 elif vc[2] == 'Z':
591 Color4f = (self.axis_z_color + (1.0,))
592 else:
593 Color4f = self.constrain_shift_color
594 else:
595 if self.type == 'OUT':
596 Color4f = self.out_color
597 elif self.type == 'FACE':
598 Color4f = self.face_color
599 elif self.type == 'EDGE':
600 Color4f = self.edge_color
601 elif self.type == 'VERT':
602 Color4f = self.vert_color
603 elif self.type == 'CENTER':
604 Color4f = self.center_color
605 elif self.type == 'PERPENDICULAR':
606 Color4f = self.perpendicular_color
607 else: # self.type == None
608 Color4f = self.out_color
610 bgl.glColor4f(*Color4f)
611 bgl.glPointSize(10)
612 bgl.glBegin(bgl.GL_POINTS)
613 bgl.glVertex3f(*self.location)
614 bgl.glEnd()
616 # draw 3d line OpenGL in the 3D View
617 bgl.glEnable(bgl.GL_DEPTH_TEST)
618 bgl.glDepthRange(0, 0.9999)
619 bgl.glColor4f(1.0, 0.8, 0.0, 1.0)
620 bgl.glLineWidth(2)
621 bgl.glEnable(bgl.GL_LINE_STIPPLE)
622 bgl.glBegin(bgl.GL_LINE_STRIP)
623 for vert_co in self.list_verts_co:
624 bgl.glVertex3f(*vert_co)
625 bgl.glVertex3f(*self.location)
626 bgl.glEnd()
628 # restore opengl defaults
629 # bgl.glPopMatrix()
630 bgl.glDepthRange(0, 1)
631 bgl.glPointSize(1)
632 bgl.glLineWidth(1)
633 bgl.glDisable(bgl.GL_BLEND)
634 bgl.glDisable(bgl.GL_LINE_STIPPLE)
635 bgl.glColor4f(0.0, 0.0, 0.0, 1.0)
638 def modal_navigation(self, context, event):
639 evkey = (event.alt, event.ctrl, event.shift, event.type, event.value)
640 if evkey in self.navigation_keys._rotate:
641 bpy.ops.view3d.rotate('INVOKE_DEFAULT')
642 return True
643 elif evkey in self.navigation_keys._move:
644 if event.shift and self.vector_constrain and \
645 self.vector_constrain[2] in {'RIGHT_SHIFT', 'LEFT_SHIFT', 'shift'}:
646 self.vector_constrain = None
647 bpy.ops.view3d.move('INVOKE_DEFAULT')
648 return True
649 else:
650 for key in self.navigation_keys._zoom:
651 if evkey == key[0:5]:
652 if True: # TODO: Use Zoom to mouse position
653 v3d = context.space_data
654 dist_range = (v3d.clip_start, v3d.clip_end)
655 rv3d = context.region_data
656 if (key[5] < 0 and rv3d.view_distance < dist_range[1]) or\
657 (key[5] > 0 and rv3d.view_distance > dist_range[0]):
658 rv3d.view_location += key[5] * (self.location - rv3d.view_location) / 6
659 rv3d.view_distance -= key[5] * rv3d.view_distance / 6
660 else:
661 bpy.ops.view3d.zoom('INVOKE_DEFAULT', delta = key[5])
662 return True
663 #break
665 return False
668 def modal(self, context, event):
669 if self.modal_navigation(context, event):
670 return {'RUNNING_MODAL'}
672 context.area.tag_redraw()
674 if event.ctrl and event.type == 'Z' and event.value == 'PRESS':
675 bpy.ops.ed.undo()
676 self.vector_constrain = None
677 self.list_verts_co = []
678 self.list_verts = []
679 self.list_edges = []
680 self.obj = bpy.context.active_object
681 self.obj_matrix = self.obj.matrix_world.copy()
682 self.bm = bmesh.from_edit_mesh(self.obj.data)
683 self.sctx.update_drawn_snap_object(self.snap_obj)
684 return {'RUNNING_MODAL'}
686 if event.type == 'MOUSEMOVE' or self.bool_update:
687 if self.rv3d.view_matrix != self.rotMat:
688 self.rotMat = self.rv3d.view_matrix.copy()
689 self.bool_update = True
690 self.cache.bedge = None
691 else:
692 self.bool_update = False
694 mval = Vector((event.mouse_region_x, event.mouse_region_y))
696 self.location, self.type, self.geom, self.len = snap_utilities(
697 self.sctx, self.obj, self.cache, context, self.obj_matrix,
698 self.bm, mval,
699 constrain = self.vector_constrain,
700 previous_vert = (self.list_verts[-1] if self.list_verts else None),
701 increment = self.incremental
703 if self.snap_to_grid and self.type == 'OUT':
704 loc = self.location / self.rd
705 self.location = Vector((round(loc.x),
706 round(loc.y),
707 round(loc.z))) * self.rd
709 if self.keyf8 and self.list_verts_co:
710 lloc = self.list_verts_co[-1]
711 view_vec, orig = self.sctx.last_ray
712 location = intersect_point_line(lloc, orig, (orig + view_vec))
713 vec = (location[0] - lloc)
714 ax, ay, az = abs(vec.x), abs(vec.y), abs(vec.z)
715 vec.x = ax > ay > az or ax > az > ay
716 vec.y = ay > ax > az or ay > az > ax
717 vec.z = az > ay > ax or az > ax > ay
718 if vec == Vector():
719 self.vector_constrain = None
720 else:
721 vc = lloc + vec
722 try:
723 if vc != self.vector_constrain[1]:
724 type = 'X' if vec.x else 'Y' if vec.y else 'Z' if vec.z else 'shift'
725 self.vector_constrain = [lloc, vc, type]
726 except:
727 type = 'X' if vec.x else 'Y' if vec.y else 'Z' if vec.z else 'shift'
728 self.vector_constrain = [lloc, vc, type]
730 if event.value == 'PRESS':
731 if self.list_verts_co and (event.ascii in CharMap.ascii or event.type in CharMap.type):
732 CharMap.modal(self, context, event)
734 elif event.type in self.constrain_keys:
735 self.bool_update = True
736 if self.vector_constrain and self.vector_constrain[2] == event.type:
737 self.vector_constrain = ()
739 else:
740 if event.shift:
741 if isinstance(self.geom, bmesh.types.BMEdge):
742 if self.list_verts:
743 loc = self.list_verts_co[-1]
744 self.vector_constrain = (loc, loc + self.geom.verts[1].co -
745 self.geom.verts[0].co, event.type)
746 else:
747 self.vector_constrain = [self.obj_matrix * v.co for
748 v in self.geom.verts] + [event.type]
749 else:
750 if self.list_verts:
751 loc = self.list_verts_co[-1]
752 else:
753 loc = self.location
754 self.vector_constrain = [loc, loc + self.constrain_keys[event.type]] + [event.type]
756 elif event.type == 'LEFTMOUSE':
757 point = self.obj_matinv * self.location
758 # with constraint the intersection can be in a different element of the selected one
759 if self.vector_constrain and self.geom:
760 geom2 = get_closest_edge(self.bm, point, 0.001)
761 else:
762 geom2 = self.geom
764 self.vector_constrain = None
765 self.list_verts_co = draw_line(self, self.obj, self.bm, geom2, point)
766 bpy.ops.ed.undo_push(message="Undo draw line*")
768 elif event.type == 'TAB':
769 self.keytab = self.keytab is False
770 if self.keytab:
771 self.sctx.set_snap_mode(False, False, True)
772 context.tool_settings.mesh_select_mode = (False, False, True)
773 else:
774 self.sctx.set_snap_mode(True, True, True)
775 context.tool_settings.mesh_select_mode = (True, True, True)
777 elif event.type == 'F8':
778 self.vector_constrain = None
779 self.keyf8 = self.keyf8 is False
781 elif event.value == 'RELEASE':
782 if event.type in {'RET', 'NUMPAD_ENTER'}:
783 if self.length_entered != "" and self.list_verts_co:
784 try:
785 text_value = bpy.utils.units.to_value(self.unit_system, 'LENGTH', self.length_entered)
786 vector = (self.location - self.list_verts_co[-1]).normalized()
787 location = (self.list_verts_co[-1] + (vector * text_value))
788 G_location = self.obj_matinv * location
789 self.list_verts_co = draw_line(self, self.obj, self.bm, self.geom, G_location)
790 self.length_entered = ""
791 self.vector_constrain = None
793 except: # ValueError:
794 self.report({'INFO'}, "Operation not supported yet")
796 elif event.type in {'RIGHTMOUSE', 'ESC'}:
797 if self.list_verts_co == [] or event.type == 'ESC':
798 del self.bm
799 del self.list_edges
800 del self.list_verts
801 del self.list_verts_co
803 bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW')
804 context.area.header_text_set("")
805 self.sctx.free()
807 #restore initial state
808 context.user_preferences.view.use_rotate_around_active = self.use_rotate_around_active
809 context.tool_settings.mesh_select_mode = self.select_mode
810 if not self.is_editmode:
811 bpy.ops.object.editmode_toggle()
813 return {'FINISHED'}
814 else:
815 self.vector_constrain = None
816 self.list_edges = []
817 self.list_verts = []
818 self.list_verts_co = []
820 a = ""
821 if self.list_verts_co:
822 if self.length_entered:
823 pos = self.line_pos
824 a = 'length: ' + self.length_entered[:pos] + '|' + self.length_entered[pos:]
825 else:
826 length = self.len
827 length = convert_distance(length, self.uinfo)
828 a = 'length: ' + length
830 context.area.header_text_set(
831 "hit: %.3f %.3f %.3f %s" % (self.location[0],
832 self.location[1], self.location[2], a)
835 return {'RUNNING_MODAL'}
837 def invoke(self, context, event):
838 if context.space_data.type == 'VIEW_3D':
839 # print('name', __name__, __package__)
840 preferences = context.user_preferences.addons[__name__].preferences
842 #Store the preferences that will be used in modal
843 self.intersect = preferences.intersect
844 self.create_face = preferences.create_face
845 self.outer_verts = preferences.outer_verts
846 self.snap_to_grid = preferences.increments_grid
848 self.out_color = preferences.out_color
849 self.face_color = preferences.face_color
850 self.edge_color = preferences.edge_color
851 self.vert_color = preferences.vert_color
852 self.center_color = preferences.center_color
853 self.perpendicular_color = preferences.perpendicular_color
854 self.constrain_shift_color = preferences.constrain_shift_color
856 self.axis_x_color = tuple(context.user_preferences.themes[0].user_interface.axis_x)
857 self.axis_y_color = tuple(context.user_preferences.themes[0].user_interface.axis_y)
858 self.axis_z_color = tuple(context.user_preferences.themes[0].user_interface.axis_z)
860 if context.mode == 'OBJECT' and \
861 (preferences.create_new_obj or context.object is None or context.object.type != 'MESH'):
863 mesh = bpy.data.meshes.new("")
864 obj = bpy.data.objects.new("", mesh)
865 context.scene.objects.link(obj)
866 context.scene.objects.active = obj
868 #Store current state
869 self.is_editmode = context.object.data.is_editmode
870 self.use_rotate_around_active = context.user_preferences.view.use_rotate_around_active
871 self.select_mode = context.tool_settings.mesh_select_mode[:]
873 #Modify the current state
874 bpy.ops.object.mode_set(mode='EDIT')
875 bpy.ops.mesh.select_all(action='DESELECT')
876 context.user_preferences.view.use_rotate_around_active = True
877 context.tool_settings.mesh_select_mode = (True, True, True)
878 context.space_data.use_occlude_geometry = True
880 #Configure the unit of measure
881 scale = context.scene.unit_settings.scale_length
882 self.unit_system = context.scene.unit_settings.system
883 separate_units = context.scene.unit_settings.use_separate
884 self.uinfo = get_units_info(scale, self.unit_system, separate_units)
886 scale /= context.space_data.grid_scale * preferences.relative_scale
887 self.rd = bpy.utils.units.to_value(self.unit_system, 'LENGTH', str(1 / scale))
889 self.incremental = bpy.utils.units.to_value(self.unit_system, 'LENGTH', str(preferences.incremental))
891 #Store values from 3d view context
892 self.rv3d = context.region_data
893 self.rotMat = self.rv3d.view_matrix.copy()
894 self.obj = bpy.context.active_object
895 self.obj_matrix = self.obj.matrix_world.copy()
896 self.obj_matinv = self.obj_matrix.inverted()
897 # self.obj_glmatrix = bgl.Buffer(bgl.GL_FLOAT, [4, 4], self.obj_matrix.transposed())
898 self.bm = bmesh.from_edit_mesh(self.obj.data) #remove at end
899 self.cache = SnapCache()
901 #init these variables to avoid errors
902 self.prevloc = Vector()
903 self.location = Vector()
904 self.list_verts = []
905 self.list_edges = []
906 self.list_verts_co = []
907 self.bool_update = False
908 self.vector_constrain = ()
909 self.navigation_keys = NavigationKeys(context)
910 self.type = 'OUT'
911 self.len = 0
912 self.length_entered = ""
913 self.line_pos = 0
914 self.bm_geom_selected = None
916 #Init event variables
917 self.keytab = False
918 self.keyf8 = False
920 #Init Snap Context
921 from snap_context import SnapContext
923 self.sctx = SnapContext(context.region, context.space_data)
924 self.sctx.set_pixel_dist(12)
925 self.sctx.use_clip_planes(True)
927 act_base = context.active_base
929 if self.outer_verts:
930 for base in context.visible_bases:
931 if base != act_base:
932 self.sctx.add_obj(base.object, base.object.matrix_world)
934 self.snap_obj = self.sctx.add_obj(act_base.object, act_base.object.matrix_world)
936 self.snap_face = context.space_data.viewport_shade not in {'BOUNDBOX', 'WIREFRAME'}
937 self.sctx.set_snap_mode(True, True, self.snap_face)
939 #modals
940 self._handle = bpy.types.SpaceView3D.draw_handler_add(self.draw_callback_px, (context,), 'WINDOW', 'POST_VIEW')
941 context.window_manager.modal_handler_add(self)
943 return {'RUNNING_MODAL'}
944 else:
945 self.report({'WARNING'}, "Active space must be a View3d")
946 return {'CANCELLED'}
949 class PanelSnapUtilities(Panel):
950 bl_space_type = "VIEW_3D"
951 bl_region_type = "TOOLS"
952 bl_category = "Snap Utilities"
953 bl_label = "Snap Utilities"
955 @classmethod
956 def poll(cls, context):
957 preferences = context.user_preferences.addons[__name__].preferences
958 return (context.mode in {'EDIT_MESH', 'OBJECT'} and
959 preferences.create_new_obj or
960 (context.object is not None and
961 context.object.type == 'MESH'))
963 def draw(self, context):
964 layout = self.layout
965 TheCol = layout.column(align=True)
966 TheCol.operator("mesh.snap_utilities_line", text="Line", icon="GREASEPENCIL")
968 addon_prefs = context.user_preferences.addons[__name__].preferences
969 expand = addon_prefs.expand_snap_settings
970 icon = "TRIA_DOWN" if expand else "TRIA_RIGHT"
972 box = layout.box()
973 box.prop(addon_prefs, "expand_snap_settings", icon=icon,
974 text="Settings:", emboss=False)
975 if expand:
976 box.prop(addon_prefs, "outer_verts")
977 box.prop(addon_prefs, "incremental")
978 box.prop(addon_prefs, "increments_grid")
979 if addon_prefs.increments_grid:
980 box.prop(addon_prefs, "relative_scale")
981 box.label(text="Line Tool:")
982 box.prop(addon_prefs, "intersect")
983 box.prop(addon_prefs, "create_face")
984 box.prop(addon_prefs, "create_new_obj")
987 # Add-ons Preferences Update Panel
989 # Define Panel classes for updating
990 panels = (
991 PanelSnapUtilities,
995 def update_panel(self, context):
996 message = "Snap Utilities Line: Updating Panel locations has failed"
997 addon_prefs = context.user_preferences.addons[__name__].preferences
998 try:
999 for panel in panels:
1000 if addon_prefs.category != panel.bl_category:
1001 if "bl_rna" in panel.__dict__:
1002 bpy.utils.unregister_class(panel)
1004 panel.bl_category = addon_prefs.category
1005 bpy.utils.register_class(panel)
1007 except Exception as e:
1008 print("\n[{}]\n{}\n\nError:\n{}".format(__name__, message, e))
1009 pass
1012 class SnapAddonPreferences(AddonPreferences):
1013 # this must match the addon name, use '__package__'
1014 # when defining this in a submodule of a python package.
1015 bl_idname = __name__
1017 intersect = BoolProperty(
1018 name="Intersect",
1019 description="Intersects created line with the existing edges, "
1020 "even if the lines do not intersect",
1021 default=True
1023 create_new_obj = BoolProperty(
1024 name="Create a new object",
1025 description="If have not a active object, or the active object "
1026 "is not in edit mode, it creates a new object",
1027 default=False
1029 create_face = BoolProperty(
1030 name="Create faces",
1031 description="Create faces defined by enclosed edges",
1032 default=False
1034 outer_verts = BoolProperty(
1035 name="Snap to outer vertices",
1036 description="The vertices of the objects are not activated also snapped",
1037 default=True
1039 expand_snap_settings = BoolProperty(
1040 name="Expand",
1041 description="Expand, to display the settings",
1042 default=False
1044 expand_color_settings = BoolProperty(
1045 name="Color Settings",
1046 description="Expand, to display the color settings",
1047 default=False
1049 increments_grid = BoolProperty(
1050 name="Increments of Grid",
1051 description="Snap to increments of grid",
1052 default=False
1054 category = StringProperty(
1055 name="Category",
1056 description="Choose a name for the category of the panel",
1057 default="Snap Utilities",
1058 update=update_panel
1060 incremental = FloatProperty(
1061 name="Incremental",
1062 description="Snap in defined increments",
1063 default=0,
1064 min=0,
1065 step=1,
1066 precision=3
1068 relative_scale = FloatProperty(
1069 name="Relative Scale",
1070 description="Value that divides the global scale",
1071 default=1,
1072 min=0,
1073 step=1,
1074 precision=3
1076 out_color = FloatVectorProperty(
1077 name="OUT",
1078 default=(0.0, 0.0, 0.0, 0.5),
1079 size=4,
1080 subtype="COLOR",
1081 min=0, max=1
1083 face_color = FloatVectorProperty(
1084 name="FACE",
1085 default=(1.0, 0.8, 0.0, 1.0),
1086 size=4,
1087 subtype="COLOR",
1088 min=0, max=1
1090 edge_color = FloatVectorProperty(
1091 name="EDGE",
1092 default=(0.0, 0.8, 1.0, 1.0),
1093 size=4,
1094 subtype="COLOR",
1095 min=0, max=1
1097 vert_color = FloatVectorProperty(
1098 name="VERT",
1099 default=(1.0, 0.5, 0.0, 1.0),
1100 size=4,
1101 subtype="COLOR",
1102 min=0, max=1
1104 center_color = FloatVectorProperty(
1105 name="CENTER",
1106 default=(1.0, 0.0, 1.0, 1.0),
1107 size=4,
1108 subtype="COLOR",
1109 min=0, max=1
1111 perpendicular_color = FloatVectorProperty(
1112 name="PERPENDICULAR",
1113 default=(0.1, 0.5, 0.5, 1.0),
1114 size=4,
1115 subtype="COLOR",
1116 min=0, max=1
1118 constrain_shift_color = FloatVectorProperty(
1119 name="SHIFT CONSTRAIN",
1120 default=(0.8, 0.5, 0.4, 1.0),
1121 size=4,
1122 subtype="COLOR",
1123 min=0, max=1
1126 def draw(self, context):
1127 layout = self.layout
1128 icon = "TRIA_DOWN" if self.expand_color_settings else "TRIA_RIGHT"
1130 box = layout.box()
1131 box.prop(self, "expand_color_settings", icon=icon, toggle=True, emboss=False)
1132 if self.expand_color_settings:
1133 split = box.split()
1135 col = split.column()
1136 col.prop(self, "out_color")
1137 col.prop(self, "constrain_shift_color")
1138 col = split.column()
1139 col.prop(self, "face_color")
1140 col.prop(self, "center_color")
1141 col = split.column()
1142 col.prop(self, "edge_color")
1143 col.prop(self, "perpendicular_color")
1144 col = split.column()
1145 col.prop(self, "vert_color")
1147 row = layout.row()
1149 col = row.column(align=True)
1150 box = col.box()
1151 box.label(text="Snap Items:")
1152 box.prop(self, "incremental")
1153 box.prop(self, "outer_verts")
1154 box.prop(self, "increments_grid")
1155 if self.increments_grid:
1156 box.prop(self, "relative_scale")
1157 else:
1158 box.separator()
1159 box.separator()
1161 col = row.column(align=True)
1162 box = col.box()
1163 box.label(text="Line Tool:")
1164 box.prop(self, "intersect")
1165 box.prop(self, "create_face")
1166 box.prop(self, "create_new_obj")
1167 box.separator()
1168 box.separator()
1170 row = layout.row()
1171 col = row.column()
1172 col.label(text="Tab Category:")
1173 col.prop(self, "category", text="")
1176 def register():
1177 bpy.utils.register_class(SnapAddonPreferences)
1178 bpy.utils.register_class(SnapUtilitiesLine)
1179 bpy.utils.register_class(PanelSnapUtilities)
1180 update_panel(None, bpy.context)
1183 def unregister():
1184 bpy.utils.unregister_class(PanelSnapUtilities)
1185 bpy.utils.unregister_class(SnapUtilitiesLine)
1186 bpy.utils.unregister_class(SnapAddonPreferences)
1189 if __name__ == "__main__":
1190 __name__ = "mesh_snap_utilities_line"
1191 register()