Fix T52833: OBJ triangulate doesn't match viewport
[blender-addons.git] / mesh_snap_utilities_line.py
blob0d1c92687d050fe1434c1295f77bceb2ba7cf5e9
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 if not hasattr(self, 'list_verts'):
355 self.list_verts = []
357 if not hasattr(self, 'list_edges'):
358 self.list_edges = []
360 split_faces = set()
362 drawing_is_dirt = False
363 update_edit_mesh = False
364 tessface = False
366 if bm_geom is None:
367 vert = bm.verts.new(location)
368 self.list_verts.append(vert)
370 elif isinstance(bm_geom, bmesh.types.BMVert):
371 if (bm_geom.co - location).length_squared < .001:
372 if self.list_verts == [] or self.list_verts[-1] != bm_geom:
373 self.list_verts.append(bm_geom)
374 else:
375 vert = bm.verts.new(location)
376 self.list_verts.append(vert)
377 drawing_is_dirt = True
379 elif isinstance(bm_geom, bmesh.types.BMEdge):
380 self.list_edges.append(bm_geom)
381 ret = intersect_point_line(location, bm_geom.verts[0].co, bm_geom.verts[1].co)
383 if (ret[0] - location).length_squared < .001:
384 if ret[1] == 0.0:
385 vert = bm_geom.verts[0]
386 elif ret[1] == 1.0:
387 vert = bm_geom.verts[1]
388 else:
389 edge, vert = bmesh.utils.edge_split(bm_geom, bm_geom.verts[0], ret[1])
390 drawing_is_dirt = True
391 self.list_verts.append(vert)
392 # self.list_edges.append(edge)
394 else: # constrain point is near
395 vert = bm.verts.new(location)
396 self.list_verts.append(vert)
397 drawing_is_dirt = True
399 elif isinstance(bm_geom, bmesh.types.BMFace):
400 split_faces.add(bm_geom)
401 vert = bm.verts.new(location)
402 self.list_verts.append(vert)
403 drawing_is_dirt = True
405 # draw, split and create face
406 if len(self.list_verts) >= 2:
407 v1, v2 = self.list_verts[-2:]
408 # v2_link_verts = [x for y in [a.verts for a in v2.link_edges] for x in y if x != v2]
409 edge = bm.edges.get([v1, v2])
410 if edge:
411 self.list_edges.append(edge)
413 else: # if v1 not in v2_link_verts:
414 if not v2.link_edges:
415 edge = bm.edges.new([v1, v2])
416 self.list_edges.append(edge)
417 drawing_is_dirt = True
418 else: # split face
419 v1_link_faces = v1.link_faces
420 v2_link_faces = v2.link_faces
421 if v1_link_faces and v2_link_faces:
422 split_faces.update(set(v1_link_faces).intersection(v2_link_faces))
424 else:
425 if v1_link_faces:
426 faces = v1_link_faces
427 co2 = v2.co.copy()
428 else:
429 faces = v2_link_faces
430 co2 = v1.co.copy()
432 for face in faces:
433 if bmesh.geometry.intersect_face_point(face, co2):
434 co = co2 - face.calc_center_median()
435 if co.dot(face.normal) < 0.001:
436 split_faces.add(face)
438 if split_faces:
439 edge = bm.edges.new([v1, v2])
440 self.list_edges.append(edge)
441 ed_list = get_loose_linked_edges(v2)
442 for face in split_faces:
443 facesp = bmesh.utils.face_split_edgenet(face, ed_list)
444 del split_faces
445 update_edit_mesh = True
446 tessface = True
447 else:
448 if self.intersect:
449 facesp = bmesh.ops.connect_vert_pair(bm, verts=[v1, v2], verts_exclude=bm.verts)
450 # print(facesp)
451 if not self.intersect or not facesp['edges']:
452 edge = bm.edges.new([v1, v2])
453 self.list_edges.append(edge)
454 drawing_is_dirt = True
455 else:
456 for edge in facesp['edges']:
457 self.list_edges.append(edge)
458 update_edit_mesh = True
459 tessface = True
461 # create face
462 if self.create_face:
463 ed_list = set(self.list_edges)
464 for edge in v2.link_edges:
465 for vert in edge.verts:
466 if vert != v2 and vert in self.list_verts:
467 ed_list.add(edge)
468 break
469 else:
470 continue
471 # Inner loop had a break, break the outer
472 break
474 ed_list.update(get_loose_linked_edges(v2))
476 bmesh.ops.edgenet_fill(bm, edges=list(ed_list))
477 update_edit_mesh = True
478 tessface = True
479 # print('face created')
480 if update_edit_mesh:
481 bmesh.update_edit_mesh(obj.data, tessface = tessface)
482 self.sctx.update_drawn_snap_object(self.snap_obj)
483 #bm.verts.index_update()
484 elif drawing_is_dirt:
485 self.obj.update_from_editmode()
486 self.sctx.update_drawn_snap_object(self.snap_obj)
488 return [obj.matrix_world * v.co for v in self.list_verts]
491 class NavigationKeys:
492 def __init__(self, context):
493 # TO DO:
494 # 'View Orbit', 'View Pan', 'NDOF Orbit View', 'NDOF Pan View'
495 self._rotate = set()
496 self._move = set()
497 self._zoom = set()
498 for key in context.window_manager.keyconfigs.user.keymaps['3D View'].keymap_items:
499 if key.idname == 'view3d.rotate':
500 #self.keys_rotate[key.id]={'Alt': key.alt, 'Ctrl': key.ctrl, 'Shift':key.shift, 'Type':key.type, 'Value':key.value}
501 self._rotate.add((key.alt, key.ctrl, key.shift, key.type, key.value))
502 if key.idname == 'view3d.move':
503 self._move.add((key.alt, key.ctrl, key.shift, key.type, key.value))
504 if key.idname == 'view3d.zoom':
505 if key.type == 'WHEELINMOUSE':
506 self._zoom.add((key.alt, key.ctrl, key.shift, 'WHEELUPMOUSE', key.value, key.properties.delta))
507 elif key.type == 'WHEELOUTMOUSE':
508 self._zoom.add((key.alt, key.ctrl, key.shift, 'WHEELDOWNMOUSE', key.value, key.properties.delta))
509 else:
510 self._zoom.add((key.alt, key.ctrl, key.shift, key.type, key.value, key.properties.delta))
513 class CharMap:
514 ascii = {
515 ".", ",", "-", "+", "1", "2", "3",
516 "4", "5", "6", "7", "8", "9", "0",
517 "c", "m", "d", "k", "h", "a",
518 " ", "/", "*", "'", "\""
519 # "="
521 type = {
522 'BACK_SPACE', 'DEL',
523 'LEFT_ARROW', 'RIGHT_ARROW'
526 @staticmethod
527 def modal(self, context, event):
528 c = event.ascii
529 if c:
530 if c == ",":
531 c = "."
532 self.length_entered = self.length_entered[:self.line_pos] + c + self.length_entered[self.line_pos:]
533 self.line_pos += 1
534 if self.length_entered:
535 if event.type == 'BACK_SPACE':
536 self.length_entered = self.length_entered[:self.line_pos - 1] + self.length_entered[self.line_pos:]
537 self.line_pos -= 1
539 elif event.type == 'DEL':
540 self.length_entered = self.length_entered[:self.line_pos] + self.length_entered[self.line_pos + 1:]
542 elif event.type == 'LEFT_ARROW':
543 self.line_pos = (self.line_pos - 1) % (len(self.length_entered) + 1)
545 elif event.type == 'RIGHT_ARROW':
546 self.line_pos = (self.line_pos + 1) % (len(self.length_entered) + 1)
549 class SnapUtilitiesLine(Operator):
550 bl_idname = "mesh.snap_utilities_line"
551 bl_label = "Line Tool"
552 bl_description = "Draw edges. Connect them to split faces"
553 bl_options = {'REGISTER', 'UNDO'}
555 constrain_keys = {
556 'X': Vector((1, 0, 0)),
557 'Y': Vector((0, 1, 0)),
558 'Z': Vector((0, 0, 1)),
559 'RIGHT_SHIFT': 'shift',
560 'LEFT_SHIFT': 'shift',
563 @classmethod
564 def poll(cls, context):
565 preferences = context.user_preferences.addons[__name__].preferences
566 return (context.mode in {'EDIT_MESH', 'OBJECT'} and
567 preferences.create_new_obj or
568 (context.object is not None and
569 context.object.type == 'MESH'))
572 def draw_callback_px(self, context):
573 # draw 3d point OpenGL in the 3D View
574 bgl.glEnable(bgl.GL_BLEND)
575 bgl.glDisable(bgl.GL_DEPTH_TEST)
576 # bgl.glPushMatrix()
577 # bgl.glMultMatrixf(self.obj_glmatrix)
579 ## if DEBUG:
580 ## mesh_drawing._store_current_shader_state(mesh_drawing.PreviousGLState)
581 ## self.screen.Draw(self.sctx._texture)
582 ## mesh_drawing._restore_shader_state(mesh_drawing.PreviousGLState)
584 if self.vector_constrain:
585 vc = self.vector_constrain
586 if hasattr(self, 'preloc') and self.type in {'VERT', 'FACE'}:
587 bgl.glColor4f(1.0, 1.0, 1.0, 0.5)
588 bgl.glPointSize(5)
589 bgl.glBegin(bgl.GL_POINTS)
590 bgl.glVertex3f(*self.preloc)
591 bgl.glEnd()
592 if vc[2] == 'X':
593 Color4f = (self.axis_x_color + (1.0,))
594 elif vc[2] == 'Y':
595 Color4f = (self.axis_y_color + (1.0,))
596 elif vc[2] == 'Z':
597 Color4f = (self.axis_z_color + (1.0,))
598 else:
599 Color4f = self.constrain_shift_color
600 else:
601 if self.type == 'OUT':
602 Color4f = self.out_color
603 elif self.type == 'FACE':
604 Color4f = self.face_color
605 elif self.type == 'EDGE':
606 Color4f = self.edge_color
607 elif self.type == 'VERT':
608 Color4f = self.vert_color
609 elif self.type == 'CENTER':
610 Color4f = self.center_color
611 elif self.type == 'PERPENDICULAR':
612 Color4f = self.perpendicular_color
613 else: # self.type == None
614 Color4f = self.out_color
616 bgl.glColor4f(*Color4f)
617 bgl.glPointSize(10)
618 bgl.glBegin(bgl.GL_POINTS)
619 bgl.glVertex3f(*self.location)
620 bgl.glEnd()
622 # draw 3d line OpenGL in the 3D View
623 bgl.glEnable(bgl.GL_DEPTH_TEST)
624 bgl.glDepthRange(0, 0.9999)
625 bgl.glColor4f(1.0, 0.8, 0.0, 1.0)
626 bgl.glLineWidth(2)
627 bgl.glEnable(bgl.GL_LINE_STIPPLE)
628 bgl.glBegin(bgl.GL_LINE_STRIP)
629 for vert_co in self.list_verts_co:
630 bgl.glVertex3f(*vert_co)
631 bgl.glVertex3f(*self.location)
632 bgl.glEnd()
634 # restore opengl defaults
635 # bgl.glPopMatrix()
636 bgl.glDepthRange(0, 1)
637 bgl.glPointSize(1)
638 bgl.glLineWidth(1)
639 bgl.glDisable(bgl.GL_BLEND)
640 bgl.glDisable(bgl.GL_LINE_STIPPLE)
641 bgl.glColor4f(0.0, 0.0, 0.0, 1.0)
644 def modal_navigation(self, context, event):
645 evkey = (event.alt, event.ctrl, event.shift, event.type, event.value)
646 if evkey in self.navigation_keys._rotate:
647 bpy.ops.view3d.rotate('INVOKE_DEFAULT')
648 return True
649 elif evkey in self.navigation_keys._move:
650 if event.shift and self.vector_constrain and \
651 self.vector_constrain[2] in {'RIGHT_SHIFT', 'LEFT_SHIFT', 'shift'}:
652 self.vector_constrain = None
653 bpy.ops.view3d.move('INVOKE_DEFAULT')
654 return True
655 else:
656 for key in self.navigation_keys._zoom:
657 if evkey == key[0:5]:
658 if True: # TODO: Use Zoom to mouse position
659 v3d = context.space_data
660 dist_range = (v3d.clip_start, v3d.clip_end)
661 rv3d = context.region_data
662 if (key[5] < 0 and rv3d.view_distance < dist_range[1]) or\
663 (key[5] > 0 and rv3d.view_distance > dist_range[0]):
664 rv3d.view_location += key[5] * (self.location - rv3d.view_location) / 6
665 rv3d.view_distance -= key[5] * rv3d.view_distance / 6
666 else:
667 bpy.ops.view3d.zoom('INVOKE_DEFAULT', delta = key[5])
668 return True
669 #break
671 return False
674 def modal(self, context, event):
675 if self.modal_navigation(context, event):
676 return {'RUNNING_MODAL'}
678 context.area.tag_redraw()
680 if event.ctrl and event.type == 'Z' and event.value == 'PRESS':
681 bpy.ops.ed.undo()
682 self.vector_constrain = None
683 self.list_verts_co = []
684 self.list_verts = []
685 self.list_edges = []
686 self.list_faces = []
687 self.obj = bpy.context.active_object
688 self.obj_matrix = self.obj.matrix_world.copy()
689 self.bm = bmesh.from_edit_mesh(self.obj.data)
690 self.sctx.update_drawn_snap_object(self.snap_obj)
691 return {'RUNNING_MODAL'}
693 if event.type == 'MOUSEMOVE' or self.bool_update:
694 if self.rv3d.view_matrix != self.rotMat:
695 self.rotMat = self.rv3d.view_matrix.copy()
696 self.bool_update = True
697 self.cache.bedge = None
698 else:
699 self.bool_update = False
701 mval = Vector((event.mouse_region_x, event.mouse_region_y))
703 self.location, self.type, self.geom, self.len = snap_utilities(
704 self.sctx, self.obj, self.cache, context, self.obj_matrix,
705 self.bm, mval,
706 constrain = self.vector_constrain,
707 previous_vert = (self.list_verts[-1] if self.list_verts else None),
708 increment = self.incremental
710 if self.snap_to_grid and self.type == 'OUT':
711 loc = self.location / self.rd
712 self.location = Vector((round(loc.x),
713 round(loc.y),
714 round(loc.z))) * self.rd
716 if self.keyf8 and self.list_verts_co:
717 lloc = self.list_verts_co[-1]
718 view_vec, orig = self.sctx.last_ray
719 location = intersect_point_line(lloc, orig, (orig + view_vec))
720 vec = (location[0] - lloc)
721 ax, ay, az = abs(vec.x), abs(vec.y), abs(vec.z)
722 vec.x = ax > ay > az or ax > az > ay
723 vec.y = ay > ax > az or ay > az > ax
724 vec.z = az > ay > ax or az > ax > ay
725 if vec == Vector():
726 self.vector_constrain = None
727 else:
728 vc = lloc + vec
729 try:
730 if vc != self.vector_constrain[1]:
731 type = 'X' if vec.x else 'Y' if vec.y else 'Z' if vec.z else 'shift'
732 self.vector_constrain = [lloc, vc, type]
733 except:
734 type = 'X' if vec.x else 'Y' if vec.y else 'Z' if vec.z else 'shift'
735 self.vector_constrain = [lloc, vc, type]
737 if event.value == 'PRESS':
738 if self.list_verts_co and (event.ascii in CharMap.ascii or event.type in CharMap.type):
739 CharMap.modal(self, context, event)
741 elif event.type in self.constrain_keys:
742 self.bool_update = True
743 if self.vector_constrain and self.vector_constrain[2] == event.type:
744 self.vector_constrain = ()
746 else:
747 if event.shift:
748 if isinstance(self.geom, bmesh.types.BMEdge):
749 if self.list_verts:
750 loc = self.list_verts_co[-1]
751 self.vector_constrain = (loc, loc + self.geom.verts[1].co -
752 self.geom.verts[0].co, event.type)
753 else:
754 self.vector_constrain = [self.obj_matrix * v.co for
755 v in self.geom.verts] + [event.type]
756 else:
757 if self.list_verts:
758 loc = self.list_verts_co[-1]
759 else:
760 loc = self.location
761 self.vector_constrain = [loc, loc + self.constrain_keys[event.type]] + [event.type]
763 elif event.type == 'LEFTMOUSE':
764 point = self.obj_matinv * self.location
765 # with constraint the intersection can be in a different element of the selected one
766 if self.vector_constrain and self.geom:
767 geom2 = get_closest_edge(self.bm, point, 0.001)
768 else:
769 geom2 = self.geom
771 self.vector_constrain = None
772 self.list_verts_co = draw_line(self, self.obj, self.bm, geom2, point)
773 bpy.ops.ed.undo_push(message="Undo draw line*")
775 elif event.type == 'TAB':
776 self.keytab = self.keytab is False
777 if self.keytab:
778 self.sctx.set_snap_mode(False, False, True)
779 context.tool_settings.mesh_select_mode = (False, False, True)
780 else:
781 self.sctx.set_snap_mode(True, True, True)
782 context.tool_settings.mesh_select_mode = (True, True, True)
784 elif event.type == 'F8':
785 self.vector_constrain = None
786 self.keyf8 = self.keyf8 is False
788 elif event.value == 'RELEASE':
789 if event.type in {'RET', 'NUMPAD_ENTER'}:
790 if self.length_entered != "" and self.list_verts_co:
791 try:
792 text_value = bpy.utils.units.to_value(self.unit_system, 'LENGTH', self.length_entered)
793 vector = (self.location - self.list_verts_co[-1]).normalized()
794 location = (self.list_verts_co[-1] + (vector * text_value))
795 G_location = self.obj_matinv * location
796 self.list_verts_co = draw_line(self, self.obj, self.bm, self.geom, G_location)
797 self.length_entered = ""
798 self.vector_constrain = None
800 except: # ValueError:
801 self.report({'INFO'}, "Operation not supported yet")
803 elif event.type in {'RIGHTMOUSE', 'ESC'}:
804 if self.list_verts_co == [] or event.type == 'ESC':
805 bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW')
806 context.tool_settings.mesh_select_mode = self.select_mode
807 context.area.header_text_set()
808 context.user_preferences.view.use_rotate_around_active = self.use_rotate_around_active
809 self.sctx.free()
810 if not self.is_editmode:
811 bpy.ops.object.editmode_toggle()
812 return {'FINISHED'}
813 else:
814 self.vector_constrain = None
815 self.list_verts = []
816 self.list_verts_co = []
817 self.list_faces = []
819 a = ""
820 if self.list_verts_co:
821 if self.length_entered:
822 pos = self.line_pos
823 a = 'length: ' + self.length_entered[:pos] + '|' + self.length_entered[pos:]
824 else:
825 length = self.len
826 length = convert_distance(length, self.uinfo)
827 a = 'length: ' + length
829 context.area.header_text_set(
830 "hit: %.3f %.3f %.3f %s" % (self.location[0],
831 self.location[1], self.location[2], a)
834 return {'RUNNING_MODAL'}
836 def invoke(self, context, event):
837 if context.space_data.type == 'VIEW_3D':
838 # print('name', __name__, __package__)
839 preferences = context.user_preferences.addons[__name__].preferences
840 create_new_obj = preferences.create_new_obj
841 if context.mode == 'OBJECT' and \
842 (create_new_obj or context.object is None or context.object.type != 'MESH'):
844 mesh = bpy.data.meshes.new("")
845 obj = bpy.data.objects.new("", mesh)
846 context.scene.objects.link(obj)
847 context.scene.objects.active = obj
849 # bgl.glEnable(bgl.GL_POINT_SMOOTH)
850 self.is_editmode = bpy.context.object.data.is_editmode
851 bpy.ops.object.mode_set(mode='EDIT')
852 context.space_data.use_occlude_geometry = True
854 self.scale = context.scene.unit_settings.scale_length
855 self.unit_system = context.scene.unit_settings.system
856 self.separate_units = context.scene.unit_settings.use_separate
857 self.uinfo = get_units_info(self.scale, self.unit_system, self.separate_units)
859 grid = context.scene.unit_settings.scale_length / context.space_data.grid_scale
860 relative_scale = preferences.relative_scale
861 self.scale = grid / relative_scale
862 self.rd = bpy.utils.units.to_value(self.unit_system, 'LENGTH', str(1 / self.scale))
864 incremental = preferences.incremental
865 self.incremental = bpy.utils.units.to_value(self.unit_system, 'LENGTH', str(incremental))
867 self.use_rotate_around_active = context.user_preferences.view.use_rotate_around_active
868 context.user_preferences.view.use_rotate_around_active = True
870 self.select_mode = context.tool_settings.mesh_select_mode[:]
871 context.tool_settings.mesh_select_mode = (True, True, True)
873 self.region = context.region
874 self.rv3d = context.region_data
875 self.rotMat = self.rv3d.view_matrix.copy()
876 self.obj = bpy.context.active_object
877 self.obj_matrix = self.obj.matrix_world.copy()
878 self.obj_matinv = self.obj_matrix.inverted()
879 # self.obj_glmatrix = bgl.Buffer(bgl.GL_FLOAT, [4, 4], self.obj_matrix.transposed())
880 self.bm = bmesh.from_edit_mesh(self.obj.data)
881 self.cache = SnapCache()
883 self.location = Vector()
884 self.list_verts = []
885 self.list_verts_co = []
886 self.bool_update = False
887 self.vector_constrain = ()
888 self.navigation_keys = NavigationKeys(context)
889 self.keytab = False
890 self.keyf8 = False
891 self.type = 'OUT'
892 self.len = 0
893 self.length_entered = ""
894 self.line_pos = 0
896 self.out_color = preferences.out_color
897 self.face_color = preferences.face_color
898 self.edge_color = preferences.edge_color
899 self.vert_color = preferences.vert_color
900 self.center_color = preferences.center_color
901 self.perpendicular_color = preferences.perpendicular_color
902 self.constrain_shift_color = preferences.constrain_shift_color
904 self.axis_x_color = tuple(context.user_preferences.themes[0].user_interface.axis_x)
905 self.axis_y_color = tuple(context.user_preferences.themes[0].user_interface.axis_y)
906 self.axis_z_color = tuple(context.user_preferences.themes[0].user_interface.axis_z)
908 self.intersect = preferences.intersect
909 self.create_face = preferences.create_face
910 self.outer_verts = preferences.outer_verts
911 self.snap_to_grid = preferences.increments_grid
913 from snap_context import SnapContext
915 self.sctx = SnapContext(context.region, context.space_data)
916 self.sctx.set_pixel_dist(12)
917 self.sctx.use_clip_planes(True)
919 act_base = context.active_base
920 self.snap_obj = self.sctx.add_obj(act_base.object, act_base.object.matrix_world)
922 if self.outer_verts:
923 for base in context.visible_bases:
924 if base != act_base:
925 self.sctx.add_obj(base.object, base.object.matrix_world)
927 ## if DEBUG:
928 ## self.screen = screenTexture()
930 bpy.ops.mesh.select_all(action='DESELECT')
932 self._handle = bpy.types.SpaceView3D.draw_handler_add(
933 self.draw_callback_px,
934 (context,),
935 'WINDOW', 'POST_VIEW'
937 context.window_manager.modal_handler_add(self)
938 return {'RUNNING_MODAL'}
939 else:
940 self.report({'WARNING'}, "Active space must be a View3d")
941 return {'CANCELLED'}
944 class PanelSnapUtilities(Panel):
945 bl_space_type = "VIEW_3D"
946 bl_region_type = "TOOLS"
947 bl_category = "Snap Utilities"
948 bl_label = "Snap Utilities"
950 @classmethod
951 def poll(cls, context):
952 preferences = context.user_preferences.addons[__name__].preferences
953 return (context.mode in {'EDIT_MESH', 'OBJECT'} and
954 preferences.create_new_obj or
955 (context.object is not None and
956 context.object.type == 'MESH'))
958 def draw(self, context):
959 layout = self.layout
960 TheCol = layout.column(align=True)
961 TheCol.operator("mesh.snap_utilities_line", text="Line", icon="GREASEPENCIL")
963 addon_prefs = context.user_preferences.addons[__name__].preferences
964 expand = addon_prefs.expand_snap_settings
965 icon = "TRIA_DOWN" if expand else "TRIA_RIGHT"
967 box = layout.box()
968 box.prop(addon_prefs, "expand_snap_settings", icon=icon,
969 text="Settings:", emboss=False)
970 if expand:
971 box.prop(addon_prefs, "outer_verts")
972 box.prop(addon_prefs, "incremental")
973 box.prop(addon_prefs, "increments_grid")
974 if addon_prefs.increments_grid:
975 box.prop(addon_prefs, "relative_scale")
976 box.label(text="Line Tool:")
977 box.prop(addon_prefs, "intersect")
978 box.prop(addon_prefs, "create_face")
979 box.prop(addon_prefs, "create_new_obj")
982 # Add-ons Preferences Update Panel
984 # Define Panel classes for updating
985 panels = (
986 PanelSnapUtilities,
990 def update_panel(self, context):
991 message = "Snap Utilities Line: Updating Panel locations has failed"
992 addon_prefs = context.user_preferences.addons[__name__].preferences
993 try:
994 for panel in panels:
995 if addon_prefs.category != panel.bl_category:
996 if "bl_rna" in panel.__dict__:
997 bpy.utils.unregister_class(panel)
999 panel.bl_category = addon_prefs.category
1000 bpy.utils.register_class(panel)
1002 except Exception as e:
1003 print("\n[{}]\n{}\n\nError:\n{}".format(__name__, message, e))
1004 pass
1007 class SnapAddonPreferences(AddonPreferences):
1008 # this must match the addon name, use '__package__'
1009 # when defining this in a submodule of a python package.
1010 bl_idname = __name__
1012 intersect = BoolProperty(
1013 name="Intersect",
1014 description="Intersects created line with the existing edges, "
1015 "even if the lines do not intersect",
1016 default=True
1018 create_new_obj = BoolProperty(
1019 name="Create a new object",
1020 description="If have not a active object, or the active object "
1021 "is not in edit mode, it creates a new object",
1022 default=False
1024 create_face = BoolProperty(
1025 name="Create faces",
1026 description="Create faces defined by enclosed edges",
1027 default=False
1029 outer_verts = BoolProperty(
1030 name="Snap to outer vertices",
1031 description="The vertices of the objects are not activated also snapped",
1032 default=True
1034 expand_snap_settings = BoolProperty(
1035 name="Expand",
1036 description="Expand, to display the settings",
1037 default=False
1039 expand_color_settings = BoolProperty(
1040 name="Color Settings",
1041 description="Expand, to display the color settings",
1042 default=False
1044 increments_grid = BoolProperty(
1045 name="Increments of Grid",
1046 description="Snap to increments of grid",
1047 default=False
1049 category = StringProperty(
1050 name="Category",
1051 description="Choose a name for the category of the panel",
1052 default="Snap Utilities",
1053 update=update_panel
1055 incremental = FloatProperty(
1056 name="Incremental",
1057 description="Snap in defined increments",
1058 default=0,
1059 min=0,
1060 step=1,
1061 precision=3
1063 relative_scale = FloatProperty(
1064 name="Relative Scale",
1065 description="Value that divides the global scale",
1066 default=1,
1067 min=0,
1068 step=1,
1069 precision=3
1071 out_color = FloatVectorProperty(
1072 name="OUT",
1073 default=(0.0, 0.0, 0.0, 0.5),
1074 size=4,
1075 subtype="COLOR",
1076 min=0, max=1
1078 face_color = FloatVectorProperty(
1079 name="FACE",
1080 default=(1.0, 0.8, 0.0, 1.0),
1081 size=4,
1082 subtype="COLOR",
1083 min=0, max=1
1085 edge_color = FloatVectorProperty(
1086 name="EDGE",
1087 default=(0.0, 0.8, 1.0, 1.0),
1088 size=4,
1089 subtype="COLOR",
1090 min=0, max=1
1092 vert_color = FloatVectorProperty(
1093 name="VERT",
1094 default=(1.0, 0.5, 0.0, 1.0),
1095 size=4,
1096 subtype="COLOR",
1097 min=0, max=1
1099 center_color = FloatVectorProperty(
1100 name="CENTER",
1101 default=(1.0, 0.0, 1.0, 1.0),
1102 size=4,
1103 subtype="COLOR",
1104 min=0, max=1
1106 perpendicular_color = FloatVectorProperty(
1107 name="PERPENDICULAR",
1108 default=(0.1, 0.5, 0.5, 1.0),
1109 size=4,
1110 subtype="COLOR",
1111 min=0, max=1
1113 constrain_shift_color = FloatVectorProperty(
1114 name="SHIFT CONSTRAIN",
1115 default=(0.8, 0.5, 0.4, 1.0),
1116 size=4,
1117 subtype="COLOR",
1118 min=0, max=1
1121 def draw(self, context):
1122 layout = self.layout
1123 icon = "TRIA_DOWN" if self.expand_color_settings else "TRIA_RIGHT"
1125 box = layout.box()
1126 box.prop(self, "expand_color_settings", icon=icon, toggle=True, emboss=False)
1127 if self.expand_color_settings:
1128 split = box.split()
1130 col = split.column()
1131 col.prop(self, "out_color")
1132 col.prop(self, "constrain_shift_color")
1133 col = split.column()
1134 col.prop(self, "face_color")
1135 col.prop(self, "center_color")
1136 col = split.column()
1137 col.prop(self, "edge_color")
1138 col.prop(self, "perpendicular_color")
1139 col = split.column()
1140 col.prop(self, "vert_color")
1142 row = layout.row()
1144 col = row.column(align=True)
1145 box = col.box()
1146 box.label(text="Snap Items:")
1147 box.prop(self, "incremental")
1148 box.prop(self, "outer_verts")
1149 box.prop(self, "increments_grid")
1150 if self.increments_grid:
1151 box.prop(self, "relative_scale")
1152 else:
1153 box.separator()
1154 box.separator()
1156 col = row.column(align=True)
1157 box = col.box()
1158 box.label(text="Line Tool:")
1159 box.prop(self, "intersect")
1160 box.prop(self, "create_face")
1161 box.prop(self, "create_new_obj")
1162 box.separator()
1163 box.separator()
1165 row = layout.row()
1166 col = row.column()
1167 col.label(text="Tab Category:")
1168 col.prop(self, "category", text="")
1171 def register():
1172 bpy.utils.register_class(SnapAddonPreferences)
1173 bpy.utils.register_class(SnapUtilitiesLine)
1174 bpy.utils.register_class(PanelSnapUtilities)
1175 update_panel(None, bpy.context)
1178 def unregister():
1179 bpy.utils.unregister_class(PanelSnapUtilities)
1180 bpy.utils.unregister_class(SnapUtilitiesLine)
1181 bpy.utils.unregister_class(SnapAddonPreferences)
1184 if __name__ == "__main__":
1185 __name__ = "mesh_snap_utilities_line"
1186 register()