Fix T62695: OBJ mtllib fails when filename contains spaces
[blender-addons.git] / mesh_extra_tools / mesh_edgetools.py
bloba5fd3e5992e09ebc54b5b0dcb88e1f64e860a5f3
1 # The Blender Edgetools is to bring CAD tools to Blender.
2 # Copyright (C) 2012 Paul Marshall
4 # ##### BEGIN GPL LICENSE BLOCK #####
6 # This program is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation, either version 3 of the License, or
9 # (at your option) any later version.
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
19 # ##### END GPL LICENSE BLOCK #####
21 # <pep8 compliant>
23 bl_info = {
24 "name": "EdgeTools",
25 "author": "Paul Marshall",
26 "version": (0, 9, 2),
27 "blender": (2, 68, 0),
28 "location": "View3D > Toolbar and View3D > Specials (W-key)",
29 "warning": "",
30 "description": "CAD style edge manipulation tools",
31 "wiki_url": "https://wiki.blender.org/index.php/Extensions:2.6/Py/"
32 "Scripts/Modeling/EdgeTools",
33 "category": "Mesh"}
36 import bpy
37 import bmesh
38 from bpy.types import (
39 Operator,
40 Menu,
42 from math import acos, pi, radians, sqrt
43 from mathutils import Matrix, Vector
44 from mathutils.geometry import (
45 distance_point_to_plane,
46 interpolate_bezier,
47 intersect_point_line,
48 intersect_line_line,
49 intersect_line_plane,
51 from bpy.props import (
52 BoolProperty,
53 IntProperty,
54 FloatProperty,
55 EnumProperty,
58 """
59 Blender EdgeTools
60 This is a toolkit for edge manipulation based on mesh manipulation
61 abilities of several CAD/CAE packages, notably CATIA's Geometric Workbench
62 from which most of these tools have a functional basis.
64 The GUI and Blender add-on structure shamelessly coded in imitation of the
65 LoopTools addon.
67 Examples:
68 - "Ortho" inspired from CATIA's line creation tool which creates a line of a
69 user specified length at a user specified angle to a curve at a chosen
70 point. The user then selects the plane the line is to be created in.
71 - "Shaft" is inspired from CATIA's tool of the same name. However, instead
72 of a curve around an axis, this will instead shaft a line, a point, or
73 a fixed radius about the selected axis.
74 - "Slice" is from CATIA's ability to split a curve on a plane. When
75 completed this be a Python equivalent with all the same basic
76 functionality, though it will sadly be a little clumsier to use due
77 to Blender's selection limitations.
79 Notes:
80 - Fillet operator and related functions removed as they didn't work
81 - Buggy parts have been hidden behind ENABLE_DEBUG global (set it to True)
82 Example: Shaft with more than two edges selected
84 Paul "BrikBot" Marshall
85 Created: January 28, 2012
86 Last Modified: October 6, 2012
88 Coded in IDLE, tested in Blender 2.6.
89 Search for "@todo" to quickly find sections that need work
91 Note: lijenstina - modified this script in preparation for merging
92 fixed the needless jumping to object mode for bmesh creation
93 causing the crash with the Slice > Rip operator
94 Removed the test operator since version 0.9.2
95 added general error handling
96 """
98 # Enable debug
99 # Set to True to have the debug prints available
100 ENABLE_DEBUG = False
103 # Quick an dirty method for getting the sign of a number:
104 def sign(number):
105 return (number > 0) - (number < 0)
108 # is_parallel
109 # Checks to see if two lines are parallel
111 def is_parallel(v1, v2, v3, v4):
112 result = intersect_line_line(v1, v2, v3, v4)
113 return result is None
116 # Handle error notifications
117 def error_handlers(self, op_name, error, reports="ERROR", func=False):
118 if self and reports:
119 self.report({'WARNING'}, reports + " (See Console for more info)")
121 is_func = "Function" if func else "Operator"
122 print("\n[Mesh EdgeTools]\n{}: {}\nError: {}\n".format(is_func, op_name, error))
125 def flip_edit_mode():
126 bpy.ops.object.editmode_toggle()
127 bpy.ops.object.editmode_toggle()
130 # check the appropriate selection condition
131 # to prevent crashes with the index out of range errors
132 # pass the bEdges and bVerts based selection tables here
133 # types: Edge, Vertex, All
134 def is_selected_enough(self, bEdges, bVerts, edges_n=1, verts_n=0, types="Edge"):
135 check = False
136 try:
137 if bEdges and types == "Edge":
138 check = (len(bEdges) >= edges_n)
139 elif bVerts and types == "Vertex":
140 check = (len(bVerts) >= verts_n)
141 elif bEdges and bVerts and types == "All":
142 check = (len(bEdges) >= edges_n and len(bVerts) >= verts_n)
144 if check is False:
145 strings = "%s Vertices and / or " % verts_n if verts_n != 0 else ""
146 self.report({'WARNING'},
147 "Needs at least " + strings + "%s Edge(s) selected. "
148 "Operation Cancelled" % edges_n)
149 flip_edit_mode()
151 return check
153 except Exception as e:
154 error_handlers(self, "is_selected_enough", e,
155 "No appropriate selection. Operation Cancelled", func=True)
156 return False
158 return False
161 # is_axial
162 # This is for the special case where the edge is parallel to an axis.
163 # The projection onto the XY plane will fail so it will have to be handled differently
165 def is_axial(v1, v2, error=0.000002):
166 vector = v2 - v1
167 # Don't need to store, but is easier to read:
168 vec0 = vector[0] > -error and vector[0] < error
169 vec1 = vector[1] > -error and vector[1] < error
170 vec2 = vector[2] > -error and vector[2] < error
171 if (vec0 or vec1) and vec2:
172 return 'Z'
173 elif vec0 and vec1:
174 return 'Y'
175 return None
178 # is_same_co
179 # For some reason "Vector = Vector" does not seem to look at the actual coordinates
181 def is_same_co(v1, v2):
182 if len(v1) != len(v2):
183 return False
184 else:
185 for co1, co2 in zip(v1, v2):
186 if co1 != co2:
187 return False
188 return True
191 def is_face_planar(face, error=0.0005):
192 for v in face.verts:
193 d = distance_point_to_plane(v.co, face.verts[0].co, face.normal)
194 if ENABLE_DEBUG:
195 print("Distance: " + str(d))
196 if d < -error or d > error:
197 return False
198 return True
201 # other_joined_edges
202 # Starts with an edge. Then scans for linked, selected edges and builds a
203 # list with them in "order", starting at one end and moving towards the other
205 def order_joined_edges(edge, edges=[], direction=1):
206 if len(edges) == 0:
207 edges.append(edge)
208 edges[0] = edge
210 if ENABLE_DEBUG:
211 print(edge, end=", ")
212 print(edges, end=", ")
213 print(direction, end="; ")
215 # Robustness check: direction cannot be zero
216 if direction == 0:
217 direction = 1
219 newList = []
220 for e in edge.verts[0].link_edges:
221 if e.select and edges.count(e) == 0:
222 if direction > 0:
223 edges.insert(0, e)
224 newList.extend(order_joined_edges(e, edges, direction + 1))
225 newList.extend(edges)
226 else:
227 edges.append(e)
228 newList.extend(edges)
229 newList.extend(order_joined_edges(e, edges, direction - 1))
231 # This will only matter at the first level:
232 direction = direction * -1
234 for e in edge.verts[1].link_edges:
235 if e.select and edges.count(e) == 0:
236 if direction > 0:
237 edges.insert(0, e)
238 newList.extend(order_joined_edges(e, edges, direction + 2))
239 newList.extend(edges)
240 else:
241 edges.append(e)
242 newList.extend(edges)
243 newList.extend(order_joined_edges(e, edges, direction))
245 if ENABLE_DEBUG:
246 print(newList, end=", ")
247 print(direction)
249 return newList
252 # --------------- GEOMETRY CALCULATION METHODS --------------
254 # distance_point_line
255 # I don't know why the mathutils.geometry API does not already have this, but
256 # it is trivial to code using the structures already in place. Instead of
257 # returning a float, I also want to know the direction vector defining the
258 # distance. Distance can be found with "Vector.length"
260 def distance_point_line(pt, line_p1, line_p2):
261 int_co = intersect_point_line(pt, line_p1, line_p2)
262 distance_vector = int_co[0] - pt
263 return distance_vector
266 # interpolate_line_line
267 # This is an experiment into a cubic Hermite spline (c-spline) for connecting
268 # two edges with edges that obey the general equation.
269 # This will return a set of point coordinates (Vectors)
271 # A good, easy to read background on the mathematics can be found at:
272 # http://cubic.org/docs/hermite.htm
274 # Right now this is . . . less than functional :P
275 # @todo
276 # - C-Spline and Bezier curves do not end on p2_co as they are supposed to.
277 # - B-Spline just fails. Epically.
278 # - Add more methods as I come across them. Who said flexibility was bad?
280 def interpolate_line_line(p1_co, p1_dir, p2_co, p2_dir, segments, tension=1,
281 typ='BEZIER', include_ends=False):
282 pieces = []
283 fraction = 1 / segments
285 # Form: p1, tangent 1, p2, tangent 2
286 if typ == 'HERMITE':
287 poly = [[2, -3, 0, 1], [1, -2, 1, 0],
288 [-2, 3, 0, 0], [1, -1, 0, 0]]
289 elif typ == 'BEZIER':
290 poly = [[-1, 3, -3, 1], [3, -6, 3, 0],
291 [1, 0, 0, 0], [-3, 3, 0, 0]]
292 p1_dir = p1_dir + p1_co
293 p2_dir = -p2_dir + p2_co
294 elif typ == 'BSPLINE':
295 # Supposed poly matrix for a cubic b-spline:
296 # poly = [[-1, 3, -3, 1], [3, -6, 3, 0],
297 # [-3, 0, 3, 0], [1, 4, 1, 0]]
298 # My own invention to try to get something that somewhat acts right
299 # This is semi-quadratic rather than fully cubic:
300 poly = [[0, -1, 0, 1], [1, -2, 1, 0],
301 [0, -1, 2, 0], [1, -1, 0, 0]]
303 if include_ends:
304 pieces.append(p1_co)
306 # Generate each point:
307 for i in range(segments - 1):
308 t = fraction * (i + 1)
309 if ENABLE_DEBUG:
310 print(t)
311 s = [t ** 3, t ** 2, t, 1]
312 h00 = (poly[0][0] * s[0]) + (poly[0][1] * s[1]) + (poly[0][2] * s[2]) + (poly[0][3] * s[3])
313 h01 = (poly[1][0] * s[0]) + (poly[1][1] * s[1]) + (poly[1][2] * s[2]) + (poly[1][3] * s[3])
314 h10 = (poly[2][0] * s[0]) + (poly[2][1] * s[1]) + (poly[2][2] * s[2]) + (poly[2][3] * s[3])
315 h11 = (poly[3][0] * s[0]) + (poly[3][1] * s[1]) + (poly[3][2] * s[2]) + (poly[3][3] * s[3])
316 pieces.append((h00 * p1_co) + (h01 * p1_dir) + (h10 * p2_co) + (h11 * p2_dir))
317 if include_ends:
318 pieces.append(p2_co)
320 # Return:
321 if len(pieces) == 0:
322 return None
323 else:
324 if ENABLE_DEBUG:
325 print(pieces)
326 return pieces
329 # intersect_line_face
331 # Calculates the coordinate of intersection of a line with a face. It returns
332 # the coordinate if one exists, otherwise None. It can only deal with tris or
333 # quads for a face. A quad does NOT have to be planar
335 Quad math and theory:
336 A quad may not be planar. Therefore the treated definition of the surface is
337 that the surface is composed of all lines bridging two other lines defined by
338 the given four points. The lines do not "cross"
340 The two lines in 3-space can defined as:
341 ┌ ┐ ┌ ┐ ┌ ┐ ┌ ┐ ┌ ┐ ┌ ┐
342 │x1│ │a11│ │b11│ │x2│ │a21│ │b21│
343 │y1│ = (1-t1)│a12│ + t1│b12│, │y2│ = (1-t2)│a22│ + t2│b22│
344 │z1│ │a13│ │b13│ │z2│ │a23│ │b23│
345 └ ┘ └ ┘ └ ┘ └ ┘ └ ┘ └ ┘
346 Therefore, the surface is the lines defined by every point alone the two
347 lines with a same "t" value (t1 = t2). This is basically R = V1 + tQ, where
348 Q = V2 - V1 therefore R = V1 + t(V2 - V1) -> R = (1 - t)V1 + tV2:
349 ┌ ┐ ┌ ┐ ┌ ┐
350 │x12│ │(1-t)a11 + t * b11│ │(1-t)a21 + t * b21│
351 │y12│ = (1 - t12)│(1-t)a12 + t * b12│ + t12│(1-t)a22 + t * b22│
352 │z12│ │(1-t)a13 + t * b13│ │(1-t)a23 + t * b23│
353 └ ┘ └ ┘ └ ┘
354 Now, the equation of our line can be likewise defined:
355 ┌ ┐ ┌ ┐ ┌ ┐
356 │x3│ │a31│ │b31│
357 │y3│ = │a32│ + t3│b32│
358 │z3│ │a33│ │b33│
359 └ ┘ └ ┘ └ ┘
360 Now we just have to find a valid solution for the two equations. This should
361 be our point of intersection. Therefore, x12 = x3 -> x, y12 = y3 -> y,
362 z12 = z3 -> z. Thus, to find that point we set the equation defining the
363 surface as equal to the equation for the line:
364 ┌ ┐ ┌ ┐ ┌ ┐ ┌ ┐
365 │(1-t)a11 + t * b11│ │(1-t)a21 + t * b21│ │a31│ │b31│
366 (1 - t12)│(1-t)a12 + t * b12│ + t12│(1-t)a22 + t * b22│ = │a32│ + t3│b32│
367 │(1-t)a13 + t * b13│ │(1-t)a23 + t * b23│ │a33│ │b33│
368 └ ┘ └ ┘ └ ┘ └ ┘
369 This leaves us with three equations, three unknowns. Solving the system by
370 hand is practically impossible, but using Mathematica we are given an insane
371 series of three equations (not reproduced here for the sake of space: see
372 http://www.mediafire.com/file/cc6m6ba3sz2b96m/intersect_line_surface.nb and
373 http://www.mediafire.com/file/0egbr5ahg14talm/intersect_line_surface2.nb for
374 Mathematica computation).
376 Additionally, the resulting series of equations may result in a div by zero
377 exception if the line in question if parallel to one of the axis or if the
378 quad is planar and parallel to either the XY, XZ, or YZ planes. However, the
379 system is still solvable but must be dealt with a little differently to avaid
380 these special cases. Because the resulting equations are a little different,
381 we have to code them differently. 00Hence the special cases.
383 Tri math and theory:
384 A triangle must be planar (three points define a plane). So we just
385 have to make sure that the line intersects inside the triangle.
387 If the point is within the triangle, then the angle between the lines that
388 connect the point to the each individual point of the triangle will be
389 equal to 2 * PI. Otherwise, if the point is outside the triangle, then the
390 sum of the angles will be less.
392 # @todo
393 # - Figure out how to deal with n-gons
394 # How the heck is a face with 8 verts defined mathematically?
395 # How do I then find the intersection point of a line with said vert?
396 # How do I know if that point is "inside" all the verts?
397 # I have no clue, and haven't been able to find anything on it so far
398 # Maybe if someone (actually reads this and) who knows could note?
401 def intersect_line_face(edge, face, is_infinite=False, error=0.000002):
402 int_co = None
404 # If we are dealing with a non-planar quad:
405 if len(face.verts) == 4 and not is_face_planar(face):
406 edgeA = face.edges[0]
407 edgeB = None
408 flipB = False
410 for i in range(len(face.edges)):
411 if face.edges[i].verts[0] not in edgeA.verts and \
412 face.edges[i].verts[1] not in edgeA.verts:
414 edgeB = face.edges[i]
415 break
417 # I haven't figured out a way to mix this in with the above. Doing so might remove a
418 # few extra instructions from having to be executed saving a few clock cycles:
419 for i in range(len(face.edges)):
420 if face.edges[i] == edgeA or face.edges[i] == edgeB:
421 continue
422 if ((edgeA.verts[0] in face.edges[i].verts and
423 edgeB.verts[1] in face.edges[i].verts) or
424 (edgeA.verts[1] in face.edges[i].verts and edgeB.verts[0] in face.edges[i].verts)):
426 flipB = True
427 break
429 # Define calculation coefficient constants:
430 # "xx1" is the x coordinate, "xx2" is the y coordinate, and "xx3" is the z coordinate
431 a11, a12, a13 = edgeA.verts[0].co[0], edgeA.verts[0].co[1], edgeA.verts[0].co[2]
432 b11, b12, b13 = edgeA.verts[1].co[0], edgeA.verts[1].co[1], edgeA.verts[1].co[2]
434 if flipB:
435 a21, a22, a23 = edgeB.verts[1].co[0], edgeB.verts[1].co[1], edgeB.verts[1].co[2]
436 b21, b22, b23 = edgeB.verts[0].co[0], edgeB.verts[0].co[1], edgeB.verts[0].co[2]
437 else:
438 a21, a22, a23 = edgeB.verts[0].co[0], edgeB.verts[0].co[1], edgeB.verts[0].co[2]
439 b21, b22, b23 = edgeB.verts[1].co[0], edgeB.verts[1].co[1], edgeB.verts[1].co[2]
440 a31, a32, a33 = edge.verts[0].co[0], edge.verts[0].co[1], edge.verts[0].co[2]
441 b31, b32, b33 = edge.verts[1].co[0], edge.verts[1].co[1], edge.verts[1].co[2]
443 # There are a bunch of duplicate "sub-calculations" inside the resulting
444 # equations for t, t12, and t3. Calculate them once and store them to
445 # reduce computational time:
446 m01 = a13 * a22 * a31
447 m02 = a12 * a23 * a31
448 m03 = a13 * a21 * a32
449 m04 = a11 * a23 * a32
450 m05 = a12 * a21 * a33
451 m06 = a11 * a22 * a33
452 m07 = a23 * a32 * b11
453 m08 = a22 * a33 * b11
454 m09 = a23 * a31 * b12
455 m10 = a21 * a33 * b12
456 m11 = a22 * a31 * b13
457 m12 = a21 * a32 * b13
458 m13 = a13 * a32 * b21
459 m14 = a12 * a33 * b21
460 m15 = a13 * a31 * b22
461 m16 = a11 * a33 * b22
462 m17 = a12 * a31 * b23
463 m18 = a11 * a32 * b23
464 m19 = a13 * a22 * b31
465 m20 = a12 * a23 * b31
466 m21 = a13 * a32 * b31
467 m22 = a23 * a32 * b31
468 m23 = a12 * a33 * b31
469 m24 = a22 * a33 * b31
470 m25 = a23 * b12 * b31
471 m26 = a33 * b12 * b31
472 m27 = a22 * b13 * b31
473 m28 = a32 * b13 * b31
474 m29 = a13 * b22 * b31
475 m30 = a33 * b22 * b31
476 m31 = a12 * b23 * b31
477 m32 = a32 * b23 * b31
478 m33 = a13 * a21 * b32
479 m34 = a11 * a23 * b32
480 m35 = a13 * a31 * b32
481 m36 = a23 * a31 * b32
482 m37 = a11 * a33 * b32
483 m38 = a21 * a33 * b32
484 m39 = a23 * b11 * b32
485 m40 = a33 * b11 * b32
486 m41 = a21 * b13 * b32
487 m42 = a31 * b13 * b32
488 m43 = a13 * b21 * b32
489 m44 = a33 * b21 * b32
490 m45 = a11 * b23 * b32
491 m46 = a31 * b23 * b32
492 m47 = a12 * a21 * b33
493 m48 = a11 * a22 * b33
494 m49 = a12 * a31 * b33
495 m50 = a22 * a31 * b33
496 m51 = a11 * a32 * b33
497 m52 = a21 * a32 * b33
498 m53 = a22 * b11 * b33
499 m54 = a32 * b11 * b33
500 m55 = a21 * b12 * b33
501 m56 = a31 * b12 * b33
502 m57 = a12 * b21 * b33
503 m58 = a32 * b21 * b33
504 m59 = a11 * b22 * b33
505 m60 = a31 * b22 * b33
506 m61 = a33 * b12 * b21
507 m62 = a32 * b13 * b21
508 m63 = a33 * b11 * b22
509 m64 = a31 * b13 * b22
510 m65 = a32 * b11 * b23
511 m66 = a31 * b12 * b23
512 m67 = b13 * b22 * b31
513 m68 = b12 * b23 * b31
514 m69 = b13 * b21 * b32
515 m70 = b11 * b23 * b32
516 m71 = b12 * b21 * b33
517 m72 = b11 * b22 * b33
518 n01 = m01 - m02 - m03 + m04 + m05 - m06
519 n02 = -m07 + m08 + m09 - m10 - m11 + m12 + m13 - m14 - m15 + m16 + m17 - m18 - \
520 m25 + m27 + m29 - m31 + m39 - m41 - m43 + m45 - m53 + m55 + m57 - m59
521 n03 = -m19 + m20 + m33 - m34 - m47 + m48
522 n04 = m21 - m22 - m23 + m24 - m35 + m36 + m37 - m38 + m49 - m50 - m51 + m52
523 n05 = m26 - m28 - m30 + m32 - m40 + m42 + m44 - m46 + m54 - m56 - m58 + m60
524 n06 = m61 - m62 - m63 + m64 + m65 - m66 - m67 + m68 + m69 - m70 - m71 + m72
525 n07 = 2 * n01 + n02 + 2 * n03 + n04 + n05
526 n08 = n01 + n02 + n03 + n06
528 # Calculate t, t12, and t3:
529 t = (n07 - sqrt(pow(-n07, 2) - 4 * (n01 + n03 + n04) * n08)) / (2 * n08)
531 # t12 can be greatly simplified by defining it with t in it:
532 # If block used to help prevent any div by zero error.
533 t12 = 0
535 if a31 == b31:
536 # The line is parallel to the z-axis:
537 if a32 == b32:
538 t12 = ((a11 - a31) + (b11 - a11) * t) / ((a21 - a11) + (a11 - a21 - b11 + b21) * t)
539 # The line is parallel to the y-axis:
540 elif a33 == b33:
541 t12 = ((a11 - a31) + (b11 - a11) * t) / ((a21 - a11) + (a11 - a21 - b11 + b21) * t)
542 # The line is along the y/z-axis but is not parallel to either:
543 else:
544 t12 = -(-(a33 - b33) * (-a32 + a12 * (1 - t) + b12 * t) + (a32 - b32) *
545 (-a33 + a13 * (1 - t) + b13 * t)) / (-(a33 - b33) *
546 ((a22 - a12) * (1 - t) + (b22 - b12) * t) + (a32 - b32) *
547 ((a23 - a13) * (1 - t) + (b23 - b13) * t))
548 elif a32 == b32:
549 # The line is parallel to the x-axis:
550 if a33 == b33:
551 t12 = ((a12 - a32) + (b12 - a12) * t) / ((a22 - a12) + (a12 - a22 - b12 + b22) * t)
552 # The line is along the x/z-axis but is not parallel to either:
553 else:
554 t12 = -(-(a33 - b33) * (-a31 + a11 * (1 - t) + b11 * t) + (a31 - b31) * (-a33 + a13 *
555 (1 - t) + b13 * t)) / (-(a33 - b33) * ((a21 - a11) * (1 - t) + (b21 - b11) * t) +
556 (a31 - b31) * ((a23 - a13) * (1 - t) + (b23 - b13) * t))
557 # The line is along the x/y-axis but is not parallel to either:
558 else:
559 t12 = -(-(a32 - b32) * (-a31 + a11 * (1 - t) + b11 * t) + (a31 - b31) * (-a32 + a12 *
560 (1 - t) + b12 * t)) / (-(a32 - b32) * ((a21 - a11) * (1 - t) + (b21 - b11) * t) +
561 (a31 - b31) * ((a22 - a21) * (1 - t) + (b22 - b12) * t))
563 # Likewise, t3 is greatly simplified by defining it in terms of t and t12:
564 # If block used to prevent a div by zero error.
565 t3 = 0
566 if a31 != b31:
567 t3 = (-a11 + a31 + (a11 - b11) * t + (a11 - a21) *
568 t12 + (a21 - a11 + b11 - b21) * t * t12) / (a31 - b31)
569 elif a32 != b32:
570 t3 = (-a12 + a32 + (a12 - b12) * t + (a12 - a22) *
571 t12 + (a22 - a12 + b12 - b22) * t * t12) / (a32 - b32)
572 elif a33 != b33:
573 t3 = (-a13 + a33 + (a13 - b13) * t + (a13 - a23) *
574 t12 + (a23 - a13 + b13 - b23) * t * t12) / (a33 - b33)
575 else:
576 if ENABLE_DEBUG:
577 print("The second edge is a zero-length edge")
578 return None
580 # Calculate the point of intersection:
581 x = (1 - t3) * a31 + t3 * b31
582 y = (1 - t3) * a32 + t3 * b32
583 z = (1 - t3) * a33 + t3 * b33
584 int_co = Vector((x, y, z))
586 if ENABLE_DEBUG:
587 print(int_co)
589 # If the line does not intersect the quad, we return "None":
590 if (t < -1 or t > 1 or t12 < -1 or t12 > 1) and not is_infinite:
591 int_co = None
593 elif len(face.verts) == 3:
594 p1, p2, p3 = face.verts[0].co, face.verts[1].co, face.verts[2].co
595 int_co = intersect_line_plane(edge.verts[0].co, edge.verts[1].co, p1, face.normal)
597 # Only check if the triangle is not being treated as an infinite plane:
598 # Math based from http://paulbourke.net/geometry/linefacet/
599 if int_co is not None and not is_infinite:
600 pA = p1 - int_co
601 pB = p2 - int_co
602 pC = p3 - int_co
603 # These must be unit vectors, else we risk a domain error:
604 pA.length = 1
605 pB.length = 1
606 pC.length = 1
607 aAB = acos(pA.dot(pB))
608 aBC = acos(pB.dot(pC))
609 aCA = acos(pC.dot(pA))
610 sumA = aAB + aBC + aCA
612 # If the point is outside the triangle:
613 if (sumA > (pi + error) and sumA < (pi - error)):
614 int_co = None
616 # This is the default case where we either have a planar quad or an n-gon
617 else:
618 int_co = intersect_line_plane(edge.verts[0].co, edge.verts[1].co,
619 face.verts[0].co, face.normal)
620 return int_co
623 # project_point_plane
624 # Projects a point onto a plane. Returns a tuple of the projection vector
625 # and the projected coordinate
627 def project_point_plane(pt, plane_co, plane_no):
628 if ENABLE_DEBUG:
629 print("project_point_plane was called")
630 proj_co = intersect_line_plane(pt, pt + plane_no, plane_co, plane_no)
631 proj_ve = proj_co - pt
632 if ENABLE_DEBUG:
633 print("project_point_plane: proj_co is {}\nproj_ve is {}".format(proj_co, proj_ve))
634 return (proj_ve, proj_co)
637 # ------------ CHAMPHER HELPER METHODS -------------
639 def is_planar_edge(edge, error=0.000002):
640 angle = edge.calc_face_angle()
641 return ((angle < error and angle > -error) or
642 (angle < (180 + error) and angle > (180 - error)))
645 # ------------- EDGE TOOL METHODS -------------------
647 # Extends an "edge" in two directions:
648 # - Requires two vertices to be selected. They do not have to form an edge
649 # - Extends "length" in both directions
651 class Extend(Operator):
652 bl_idname = "mesh.edgetools_extend"
653 bl_label = "Extend"
654 bl_description = "Extend the selected edges of vertex pairs"
655 bl_options = {'REGISTER', 'UNDO'}
657 di1: BoolProperty(
658 name="Forwards",
659 description="Extend the edge forwards",
660 default=True
662 di2: BoolProperty(
663 name="Backwards",
664 description="Extend the edge backwards",
665 default=False
667 length: FloatProperty(
668 name="Length",
669 description="Length to extend the edge",
670 min=0.0, max=1024.0,
671 default=1.0
674 def draw(self, context):
675 layout = self.layout
677 row = layout.row(align=True)
678 row.prop(self, "di1", toggle=True)
679 row.prop(self, "di2", toggle=True)
681 layout.prop(self, "length")
683 @classmethod
684 def poll(cls, context):
685 ob = context.active_object
686 return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
688 def invoke(self, context, event):
689 return self.execute(context)
691 def execute(self, context):
692 try:
693 me = context.object.data
694 bm = bmesh.from_edit_mesh(me)
695 bm.normal_update()
697 bEdges = bm.edges
698 bVerts = bm.verts
700 edges = [e for e in bEdges if e.select]
701 verts = [v for v in bVerts if v.select]
703 if not is_selected_enough(self, edges, 0, edges_n=1, verts_n=0, types="Edge"):
704 return {'CANCELLED'}
706 if len(edges) > 0:
707 for e in edges:
708 vector = e.verts[0].co - e.verts[1].co
709 vector.length = self.length
711 if self.di1:
712 v = bVerts.new()
713 if (vector[0] + vector[1] + vector[2]) < 0:
714 v.co = e.verts[1].co - vector
715 newE = bEdges.new((e.verts[1], v))
716 bEdges.ensure_lookup_table()
717 else:
718 v.co = e.verts[0].co + vector
719 newE = bEdges.new((e.verts[0], v))
720 bEdges.ensure_lookup_table()
721 if self.di2:
722 v = bVerts.new()
723 if (vector[0] + vector[1] + vector[2]) < 0:
724 v.co = e.verts[0].co + vector
725 newE = bEdges.new((e.verts[0], v))
726 bEdges.ensure_lookup_table()
727 else:
728 v.co = e.verts[1].co - vector
729 newE = bEdges.new((e.verts[1], v))
730 bEdges.ensure_lookup_table()
731 else:
732 vector = verts[0].co - verts[1].co
733 vector.length = self.length
735 if self.di1:
736 v = bVerts.new()
737 if (vector[0] + vector[1] + vector[2]) < 0:
738 v.co = verts[1].co - vector
739 e = bEdges.new((verts[1], v))
740 bEdges.ensure_lookup_table()
741 else:
742 v.co = verts[0].co + vector
743 e = bEdges.new((verts[0], v))
744 bEdges.ensure_lookup_table()
745 if self.di2:
746 v = bVerts.new()
747 if (vector[0] + vector[1] + vector[2]) < 0:
748 v.co = verts[0].co + vector
749 e = bEdges.new((verts[0], v))
750 bEdges.ensure_lookup_table()
751 else:
752 v.co = verts[1].co - vector
753 e = bEdges.new((verts[1], v))
754 bEdges.ensure_lookup_table()
756 bmesh.update_edit_mesh(me)
758 except Exception as e:
759 error_handlers(self, "mesh.edgetools_extend", e,
760 reports="Extend Operator failed", func=False)
761 return {'CANCELLED'}
763 return {'FINISHED'}
766 # Creates a series of edges between two edges using spline interpolation.
767 # This basically just exposes existing functionality in addition to some
768 # other common methods: Hermite (c-spline), Bezier, and b-spline. These
769 # alternates I coded myself after some extensive research into spline theory
771 # @todo Figure out what's wrong with the Blender bezier interpolation
773 class Spline(Operator):
774 bl_idname = "mesh.edgetools_spline"
775 bl_label = "Spline"
776 bl_description = "Create a spline interplopation between two edges"
777 bl_options = {'REGISTER', 'UNDO'}
779 alg: EnumProperty(
780 name="Spline Algorithm",
781 items=[('Blender', "Blender", "Interpolation provided through mathutils.geometry"),
782 ('Hermite', "C-Spline", "C-spline interpolation"),
783 ('Bezier', "Bezier", "Bezier interpolation"),
784 ('B-Spline', "B-Spline", "B-Spline interpolation")],
785 default='Bezier'
787 segments: IntProperty(
788 name="Segments",
789 description="Number of segments to use in the interpolation",
790 min=2, max=4096,
791 soft_max=1024,
792 default=32
794 flip1: BoolProperty(
795 name="Flip Edge",
796 description="Flip the direction of the spline on Edge 1",
797 default=False
799 flip2: BoolProperty(
800 name="Flip Edge",
801 description="Flip the direction of the spline on Edge 2",
802 default=False
804 ten1: FloatProperty(
805 name="Tension",
806 description="Tension on Edge 1",
807 min=-4096.0, max=4096.0,
808 soft_min=-8.0, soft_max=8.0,
809 default=1.0
811 ten2: FloatProperty(
812 name="Tension",
813 description="Tension on Edge 2",
814 min=-4096.0, max=4096.0,
815 soft_min=-8.0, soft_max=8.0,
816 default=1.0
819 def draw(self, context):
820 layout = self.layout
822 layout.prop(self, "alg")
823 layout.prop(self, "segments")
825 layout.label(text="Edge 1:")
826 split = layout.split(percentage=0.8, align=True)
827 split.prop(self, "ten1")
828 split.prop(self, "flip1", text="", icon="ALIGN", toggle=True)
830 layout.label(text="Edge 2:")
831 split = layout.split(percentage=0.8, align=True)
832 split.prop(self, "ten2")
833 split.prop(self, "flip2", text="", icon="ALIGN", toggle=True)
835 @classmethod
836 def poll(cls, context):
837 ob = context.active_object
838 return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
840 def invoke(self, context, event):
841 return self.execute(context)
843 def execute(self, context):
844 try:
845 me = context.object.data
846 bm = bmesh.from_edit_mesh(me)
847 bm.normal_update()
849 bEdges = bm.edges
850 bVerts = bm.verts
852 seg = self.segments
853 edges = [e for e in bEdges if e.select]
855 if not is_selected_enough(self, edges, 0, edges_n=2, verts_n=0, types="Edge"):
856 return {'CANCELLED'}
858 verts = [edges[v // 2].verts[v % 2] for v in range(4)]
860 if self.flip1:
861 v1 = verts[1]
862 p1_co = verts[1].co
863 p1_dir = verts[1].co - verts[0].co
864 else:
865 v1 = verts[0]
866 p1_co = verts[0].co
867 p1_dir = verts[0].co - verts[1].co
868 if self.ten1 < 0:
869 p1_dir = -1 * p1_dir
870 p1_dir.length = -self.ten1
871 else:
872 p1_dir.length = self.ten1
874 if self.flip2:
875 v2 = verts[3]
876 p2_co = verts[3].co
877 p2_dir = verts[2].co - verts[3].co
878 else:
879 v2 = verts[2]
880 p2_co = verts[2].co
881 p2_dir = verts[3].co - verts[2].co
882 if self.ten2 < 0:
883 p2_dir = -1 * p2_dir
884 p2_dir.length = -self.ten2
885 else:
886 p2_dir.length = self.ten2
888 # Get the interploted coordinates:
889 if self.alg == 'Blender':
890 pieces = interpolate_bezier(
891 p1_co, p1_dir, p2_dir, p2_co, self.segments
893 elif self.alg == 'Hermite':
894 pieces = interpolate_line_line(
895 p1_co, p1_dir, p2_co, p2_dir, self.segments, 1, 'HERMITE'
897 elif self.alg == 'Bezier':
898 pieces = interpolate_line_line(
899 p1_co, p1_dir, p2_co, p2_dir, self.segments, 1, 'BEZIER'
901 elif self.alg == 'B-Spline':
902 pieces = interpolate_line_line(
903 p1_co, p1_dir, p2_co, p2_dir, self.segments, 1, 'BSPLINE'
906 verts = []
907 verts.append(v1)
908 # Add vertices and set the points:
909 for i in range(seg - 1):
910 v = bVerts.new()
911 v.co = pieces[i]
912 bVerts.ensure_lookup_table()
913 verts.append(v)
914 verts.append(v2)
915 # Connect vertices:
916 for i in range(seg):
917 e = bEdges.new((verts[i], verts[i + 1]))
918 bEdges.ensure_lookup_table()
920 bmesh.update_edit_mesh(me)
922 except Exception as e:
923 error_handlers(self, "mesh.edgetools_spline", e,
924 reports="Spline Operator failed", func=False)
925 return {'CANCELLED'}
927 return {'FINISHED'}
930 # Creates edges normal to planes defined between each of two edges and the
931 # normal or the plane defined by those two edges.
932 # - Select two edges. The must form a plane.
933 # - On running the script, eight edges will be created. Delete the
934 # extras that you don't need.
935 # - The length of those edges is defined by the variable "length"
937 # @todo Change method from a cross product to a rotation matrix to make the
938 # angle part work.
939 # --- todo completed 2/4/2012, but still needs work ---
940 # @todo Figure out a way to make +/- predictable
941 # - Maybe use angle between edges and vector direction definition?
942 # --- TODO COMPLETED ON 2/9/2012 ---
944 class Ortho(Operator):
945 bl_idname = "mesh.edgetools_ortho"
946 bl_label = "Angle Off Edge"
947 bl_description = "Creates new edges within an angle from vertices of selected edges"
948 bl_options = {'REGISTER', 'UNDO'}
950 vert1: BoolProperty(
951 name="Vertice 1",
952 description="Enable edge creation for Vertice 1",
953 default=True
955 vert2: BoolProperty(
956 name="Vertice 2",
957 description="Enable edge creation for Vertice 2",
958 default=True
960 vert3: BoolProperty(
961 name="Vertice 3",
962 description="Enable edge creation for Vertice 3",
963 default=True
965 vert4: BoolProperty(
966 name="Vertice 4",
967 description="Enable edge creation for Vertice 4",
968 default=True
970 pos: BoolProperty(
971 name="Positive",
972 description="Enable creation of positive direction edges",
973 default=True
975 neg: BoolProperty(
976 name="Negative",
977 description="Enable creation of negative direction edges",
978 default=True
980 angle: FloatProperty(
981 name="Angle",
982 description="Define the angle off of the originating edge",
983 min=0.0, max=180.0,
984 default=90.0
986 length: FloatProperty(
987 name="Length",
988 description="Length of created edges",
989 min=0.0, max=1024.0,
990 default=1.0
992 # For when only one edge is selected (Possible feature to be testd):
993 plane: EnumProperty(
994 name="Plane",
995 items=[("XY", "X-Y Plane", "Use the X-Y plane as the plane of creation"),
996 ("XZ", "X-Z Plane", "Use the X-Z plane as the plane of creation"),
997 ("YZ", "Y-Z Plane", "Use the Y-Z plane as the plane of creation")],
998 default="XY"
1001 def draw(self, context):
1002 layout = self.layout
1004 layout.label(text="Creation:")
1005 split = layout.split()
1006 col = split.column()
1008 col.prop(self, "vert1", toggle=True)
1009 col.prop(self, "vert2", toggle=True)
1011 col = split.column()
1012 col.prop(self, "vert3", toggle=True)
1013 col.prop(self, "vert4", toggle=True)
1015 layout.label(text="Direction:")
1016 row = layout.row(align=False)
1017 row.alignment = 'EXPAND'
1018 row.prop(self, "pos")
1019 row.prop(self, "neg")
1021 layout.separator()
1023 col = layout.column(align=True)
1024 col.prop(self, "angle")
1025 col.prop(self, "length")
1027 @classmethod
1028 def poll(cls, context):
1029 ob = context.active_object
1030 return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
1032 def invoke(self, context, event):
1033 return self.execute(context)
1035 def execute(self, context):
1036 try:
1037 me = context.object.data
1038 bm = bmesh.from_edit_mesh(me)
1039 bm.normal_update()
1041 bVerts = bm.verts
1042 bEdges = bm.edges
1043 edges = [e for e in bEdges if e.select]
1044 vectors = []
1046 if not is_selected_enough(self, edges, 0, edges_n=2, verts_n=0, types="Edge"):
1047 return {'CANCELLED'}
1049 verts = [edges[0].verts[0],
1050 edges[0].verts[1],
1051 edges[1].verts[0],
1052 edges[1].verts[1]]
1054 cos = intersect_line_line(verts[0].co, verts[1].co, verts[2].co, verts[3].co)
1056 # If the two edges are parallel:
1057 if cos is None:
1058 self.report({'WARNING'},
1059 "Selected lines are parallel: results may be unpredictable")
1060 vectors.append(verts[0].co - verts[1].co)
1061 vectors.append(verts[0].co - verts[2].co)
1062 vectors.append(vectors[0].cross(vectors[1]))
1063 vectors.append(vectors[2].cross(vectors[0]))
1064 vectors.append(-vectors[3])
1065 else:
1066 # Warn the user if they have not chosen two planar edges:
1067 if not is_same_co(cos[0], cos[1]):
1068 self.report({'WARNING'},
1069 "Selected lines are not planar: results may be unpredictable")
1071 # This makes the +/- behavior predictable:
1072 if (verts[0].co - cos[0]).length < (verts[1].co - cos[0]).length:
1073 verts[0], verts[1] = verts[1], verts[0]
1074 if (verts[2].co - cos[0]).length < (verts[3].co - cos[0]).length:
1075 verts[2], verts[3] = verts[3], verts[2]
1077 vectors.append(verts[0].co - verts[1].co)
1078 vectors.append(verts[2].co - verts[3].co)
1080 # Normal of the plane formed by vector1 and vector2:
1081 vectors.append(vectors[0].cross(vectors[1]))
1083 # Possible directions:
1084 vectors.append(vectors[2].cross(vectors[0]))
1085 vectors.append(vectors[1].cross(vectors[2]))
1087 # Set the length:
1088 vectors[3].length = self.length
1089 vectors[4].length = self.length
1091 # Perform any additional rotations:
1092 matrix = Matrix.Rotation(radians(90 + self.angle), 3, vectors[2])
1093 vectors.append(matrix * -vectors[3]) # vectors[5]
1094 matrix = Matrix.Rotation(radians(90 - self.angle), 3, vectors[2])
1095 vectors.append(matrix * vectors[4]) # vectors[6]
1096 vectors.append(matrix * vectors[3]) # vectors[7]
1097 matrix = Matrix.Rotation(radians(90 + self.angle), 3, vectors[2])
1098 vectors.append(matrix * -vectors[4]) # vectors[8]
1100 # Perform extrusions and displacements:
1101 # There will be a total of 8 extrusions. One for each vert of each edge.
1102 # It looks like an extrusion will add the new vert to the end of the verts
1103 # list and leave the rest in the same location.
1104 # -- EDIT --
1105 # It looks like I might be able to do this within "bpy.data" with the ".add" function
1107 for v in range(len(verts)):
1108 vert = verts[v]
1109 if ((v == 0 and self.vert1) or (v == 1 and self.vert2) or
1110 (v == 2 and self.vert3) or (v == 3 and self.vert4)):
1112 if self.pos:
1113 new = bVerts.new()
1114 new.co = vert.co - vectors[5 + (v // 2) + ((v % 2) * 2)]
1115 bVerts.ensure_lookup_table()
1116 bEdges.new((vert, new))
1117 bEdges.ensure_lookup_table()
1118 if self.neg:
1119 new = bVerts.new()
1120 new.co = vert.co + vectors[5 + (v // 2) + ((v % 2) * 2)]
1121 bVerts.ensure_lookup_table()
1122 bEdges.new((vert, new))
1123 bEdges.ensure_lookup_table()
1125 bmesh.update_edit_mesh(me)
1126 except Exception as e:
1127 error_handlers(self, "mesh.edgetools_ortho", e,
1128 reports="Angle Off Edge Operator failed", func=False)
1129 return {'CANCELLED'}
1131 return {'FINISHED'}
1134 # Usage:
1135 # Select an edge and a point or an edge and specify the radius (default is 1 BU)
1136 # You can select two edges but it might be unpredictable which edge it revolves
1137 # around so you might have to play with the switch
1139 class Shaft(Operator):
1140 bl_idname = "mesh.edgetools_shaft"
1141 bl_label = "Shaft"
1142 bl_description = "Create a shaft mesh around an axis"
1143 bl_options = {'REGISTER', 'UNDO'}
1145 # Selection defaults:
1146 shaftType = 0
1148 # For tracking if the user has changed selection:
1149 last_edge: IntProperty(
1150 name="Last Edge",
1151 description="Tracks if user has changed selected edges",
1152 min=0, max=1,
1153 default=0
1155 last_flip = False
1157 edge: IntProperty(
1158 name="Edge",
1159 description="Edge to shaft around",
1160 min=0, max=1,
1161 default=0
1163 flip: BoolProperty(
1164 name="Flip Second Edge",
1165 description="Flip the perceived direction of the second edge",
1166 default=False
1168 radius: FloatProperty(
1169 name="Radius",
1170 description="Shaft Radius",
1171 min=0.0, max=1024.0,
1172 default=1.0
1174 start: FloatProperty(
1175 name="Starting Angle",
1176 description="Angle to start the shaft at",
1177 min=-360.0, max=360.0,
1178 default=0.0
1180 finish: FloatProperty(
1181 name="Ending Angle",
1182 description="Angle to end the shaft at",
1183 min=-360.0, max=360.0,
1184 default=360.0
1186 segments: IntProperty(
1187 name="Shaft Segments",
1188 description="Number of segments to use in the shaft",
1189 min=1, max=4096,
1190 soft_max=512,
1191 default=32
1194 def draw(self, context):
1195 layout = self.layout
1197 if self.shaftType == 0:
1198 layout.prop(self, "edge")
1199 layout.prop(self, "flip")
1200 elif self.shaftType == 3:
1201 layout.prop(self, "radius")
1203 layout.prop(self, "segments")
1204 layout.prop(self, "start")
1205 layout.prop(self, "finish")
1207 @classmethod
1208 def poll(cls, context):
1209 ob = context.active_object
1210 return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
1212 def invoke(self, context, event):
1213 # Make sure these get reset each time we run:
1214 self.last_edge = 0
1215 self.edge = 0
1217 return self.execute(context)
1219 def execute(self, context):
1220 try:
1221 me = context.object.data
1222 bm = bmesh.from_edit_mesh(me)
1223 bm.normal_update()
1225 bFaces = bm.faces
1226 bEdges = bm.edges
1227 bVerts = bm.verts
1229 active = None
1230 edges, verts = [], []
1232 # Pre-caclulated values:
1233 rotRange = [radians(self.start), radians(self.finish)]
1234 rads = radians((self.finish - self.start) / self.segments)
1236 numV = self.segments + 1
1237 numE = self.segments
1239 edges = [e for e in bEdges if e.select]
1241 # Robustness check: there should at least be one edge selected
1242 if not is_selected_enough(self, edges, 0, edges_n=1, verts_n=0, types="Edge"):
1243 return {'CANCELLED'}
1245 # If two edges are selected:
1246 if len(edges) == 2:
1247 # default:
1248 edge = [0, 1]
1249 vert = [0, 1]
1251 # By default, we want to shaft around the last selected edge (it
1252 # will be the active edge). We know we are using the default if
1253 # the user has not changed which edge is being shafted around (as
1254 # is tracked by self.last_edge). When they are not the same, then
1255 # the user has changed selection.
1256 # We then need to make sure that the active object really is an edge
1257 # (robustness check)
1258 # Finally, if the active edge is not the initial one, we flip them
1259 # and have the GUI reflect that
1260 if self.last_edge == self.edge:
1261 if isinstance(bm.select_history.active, bmesh.types.BMEdge):
1262 if bm.select_history.active != edges[edge[0]]:
1263 self.last_edge, self.edge = edge[1], edge[1]
1264 edge = [edge[1], edge[0]]
1265 else:
1266 flip_edit_mode()
1267 self.report({'WARNING'},
1268 "Active geometry is not an edge. Operation Cancelled")
1269 return {'CANCELLED'}
1270 elif self.edge == 1:
1271 edge = [1, 0]
1273 verts.append(edges[edge[0]].verts[0])
1274 verts.append(edges[edge[0]].verts[1])
1276 if self.flip:
1277 verts = [1, 0]
1279 verts.append(edges[edge[1]].verts[vert[0]])
1280 verts.append(edges[edge[1]].verts[vert[1]])
1282 self.shaftType = 0
1283 # If there is more than one edge selected:
1284 # There are some issues with it ATM, so don't expose is it to normal users
1285 # @todo Fix edge connection ordering issue
1286 elif ENABLE_DEBUG and len(edges) > 2:
1287 if isinstance(bm.select_history.active, bmesh.types.BMEdge):
1288 active = bm.select_history.active
1289 edges.remove(active)
1290 # Get all the verts:
1291 # edges = order_joined_edges(edges[0])
1292 verts = []
1293 for e in edges:
1294 if verts.count(e.verts[0]) == 0:
1295 verts.append(e.verts[0])
1296 if verts.count(e.verts[1]) == 0:
1297 verts.append(e.verts[1])
1298 else:
1299 flip_edit_mode()
1300 self.report({'WARNING'},
1301 "Active geometry is not an edge. Operation Cancelled")
1302 return {'CANCELLED'}
1303 self.shaftType = 1
1304 else:
1305 verts.append(edges[0].verts[0])
1306 verts.append(edges[0].verts[1])
1308 for v in bVerts:
1309 if v.select and verts.count(v) == 0:
1310 verts.append(v)
1311 v.select = False
1312 if len(verts) == 2:
1313 self.shaftType = 3
1314 else:
1315 self.shaftType = 2
1317 # The vector denoting the axis of rotation:
1318 if self.shaftType == 1:
1319 axis = active.verts[1].co - active.verts[0].co
1320 else:
1321 axis = verts[1].co - verts[0].co
1323 # We will need a series of rotation matrices. We could use one which
1324 # would be faster but also might cause propagation of error
1325 # matrices = []
1326 # for i in range(numV):
1327 # matrices.append(Matrix.Rotation((rads * i) + rotRange[0], 3, axis))
1328 matrices = [Matrix.Rotation((rads * i) + rotRange[0], 3, axis) for i in range(numV)]
1330 # New vertice coordinates:
1331 verts_out = []
1333 # If two edges were selected:
1334 # - If the lines are not parallel, then it will create a cone-like shaft
1335 if self.shaftType == 0:
1336 for i in range(len(verts) - 2):
1337 init_vec = distance_point_line(verts[i + 2].co, verts[0].co, verts[1].co)
1338 co = init_vec + verts[i + 2].co
1339 # These will be rotated about the origin so will need to be shifted:
1340 for j in range(numV):
1341 verts_out.append(co - (matrices[j] * init_vec))
1342 elif self.shaftType == 1:
1343 for i in verts:
1344 init_vec = distance_point_line(i.co, active.verts[0].co, active.verts[1].co)
1345 co = init_vec + i.co
1346 # These will be rotated about the origin so will need to be shifted:
1347 for j in range(numV):
1348 verts_out.append(co - (matrices[j] * init_vec))
1349 # Else if a line and a point was selected:
1350 elif self.shaftType == 2:
1351 init_vec = distance_point_line(verts[2].co, verts[0].co, verts[1].co)
1352 # These will be rotated about the origin so will need to be shifted:
1353 verts_out = [
1354 (verts[i].co - (matrices[j] * init_vec)) for i in range(2) for j in range(numV)
1356 else:
1357 # Else the above are not possible, so we will just use the edge:
1358 # - The vector defined by the edge is the normal of the plane for the shaft
1359 # - The shaft will have radius "radius"
1360 if is_axial(verts[0].co, verts[1].co) is None:
1361 proj = (verts[1].co - verts[0].co)
1362 proj[2] = 0
1363 norm = proj.cross(verts[1].co - verts[0].co)
1364 vec = norm.cross(verts[1].co - verts[0].co)
1365 vec.length = self.radius
1366 elif is_axial(verts[0].co, verts[1].co) == 'Z':
1367 vec = verts[0].co + Vector((0, 0, self.radius))
1368 else:
1369 vec = verts[0].co + Vector((0, self.radius, 0))
1370 init_vec = distance_point_line(vec, verts[0].co, verts[1].co)
1371 # These will be rotated about the origin so will need to be shifted:
1372 verts_out = [
1373 (verts[i].co - (matrices[j] * init_vec)) for i in range(2) for j in range(numV)
1376 # We should have the coordinates for a bunch of new verts
1377 # Now add the verts and build the edges and then the faces
1379 newVerts = []
1381 if self.shaftType == 1:
1382 # Vertices:
1383 for i in range(numV * len(verts)):
1384 new = bVerts.new()
1385 new.co = verts_out[i]
1386 bVerts.ensure_lookup_table()
1387 new.select = True
1388 newVerts.append(new)
1389 # Edges:
1390 for i in range(numE):
1391 for j in range(len(verts)):
1392 e = bEdges.new((newVerts[i + (numV * j)], newVerts[i + (numV * j) + 1]))
1393 bEdges.ensure_lookup_table()
1394 e.select = True
1395 for i in range(numV):
1396 for j in range(len(verts) - 1):
1397 e = bEdges.new((newVerts[i + (numV * j)], newVerts[i + (numV * (j + 1))]))
1398 bEdges.ensure_lookup_table()
1399 e.select = True
1401 # Faces: There is a problem with this right now
1403 for i in range(len(edges)):
1404 for j in range(numE):
1405 f = bFaces.new((newVerts[i], newVerts[i + 1],
1406 newVerts[i + (numV * j) + 1], newVerts[i + (numV * j)]))
1407 f.normal_update()
1409 else:
1410 # Vertices:
1411 for i in range(numV * 2):
1412 new = bVerts.new()
1413 new.co = verts_out[i]
1414 new.select = True
1415 bVerts.ensure_lookup_table()
1416 newVerts.append(new)
1417 # Edges:
1418 for i in range(numE):
1419 e = bEdges.new((newVerts[i], newVerts[i + 1]))
1420 e.select = True
1421 bEdges.ensure_lookup_table()
1422 e = bEdges.new((newVerts[i + numV], newVerts[i + numV + 1]))
1423 e.select = True
1424 bEdges.ensure_lookup_table()
1425 for i in range(numV):
1426 e = bEdges.new((newVerts[i], newVerts[i + numV]))
1427 e.select = True
1428 bEdges.ensure_lookup_table()
1429 # Faces:
1430 for i in range(numE):
1431 f = bFaces.new((newVerts[i], newVerts[i + 1],
1432 newVerts[i + numV + 1], newVerts[i + numV]))
1433 bFaces.ensure_lookup_table()
1434 f.normal_update()
1436 bmesh.update_edit_mesh(me)
1438 except Exception as e:
1439 error_handlers(self, "mesh.edgetools_shaft", e,
1440 reports="Shaft Operator failed", func=False)
1441 return {'CANCELLED'}
1443 return {'FINISHED'}
1446 # "Slices" edges crossing a plane defined by a face
1448 class Slice(Operator):
1449 bl_idname = "mesh.edgetools_slice"
1450 bl_label = "Slice"
1451 bl_description = "Cut edges at the plane defined by a selected face"
1452 bl_options = {'REGISTER', 'UNDO'}
1454 make_copy: BoolProperty(
1455 name="Make Copy",
1456 description="Make new vertices at intersection points instead of splitting the edge",
1457 default=False
1459 rip: BoolProperty(
1460 name="Rip",
1461 description="Split into two edges that DO NOT share an intersection vertex",
1462 default=True
1464 pos: BoolProperty(
1465 name="Positive",
1466 description="Remove the portion on the side of the face normal",
1467 default=False
1469 neg: BoolProperty(
1470 name="Negative",
1471 description="Remove the portion on the side opposite of the face normal",
1472 default=False
1475 def draw(self, context):
1476 layout = self.layout
1478 layout.prop(self, "make_copy")
1479 if not self.make_copy:
1480 layout.prop(self, "rip")
1481 layout.label(text="Remove Side:")
1482 layout.prop(self, "pos")
1483 layout.prop(self, "neg")
1485 @classmethod
1486 def poll(cls, context):
1487 ob = context.active_object
1488 return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
1490 def invoke(self, context, event):
1491 return self.execute(context)
1493 def execute(self, context):
1494 try:
1495 me = context.object.data
1496 bm = bmesh.from_edit_mesh(me)
1497 bm.normal_update()
1499 bVerts = bm.verts
1500 bEdges = bm.edges
1501 bFaces = bm.faces
1503 face, normal = None, None
1505 # Find the selected face. This will provide the plane to project onto:
1506 # - First check to use the active face. Allows users to just
1507 # select a bunch of faces with the last being the cutting plane
1508 # - If that fails, then use the first found selected face in the BMesh face list
1509 if isinstance(bm.select_history.active, bmesh.types.BMFace):
1510 face = bm.select_history.active
1511 normal = bm.select_history.active.normal
1512 bm.select_history.active.select = False
1513 else:
1514 for f in bFaces:
1515 if f.select:
1516 face = f
1517 normal = f.normal
1518 f.select = False
1519 break
1521 # If we don't find a selected face exit:
1522 if face is None:
1523 flip_edit_mode()
1524 self.report({'WARNING'},
1525 "Please select a face as the cutting plane. Operation Cancelled")
1526 return {'CANCELLED'}
1528 # Warn the user if they are using an n-gon might lead to some odd results
1529 elif len(face.verts) > 4 and not is_face_planar(face):
1530 self.report({'WARNING'},
1531 "Selected face is an N-gon. Results may be unpredictable")
1533 if ENABLE_DEBUG:
1534 dbg = 0
1535 print("Number of Edges: ", len(bEdges))
1537 for e in bEdges:
1538 if ENABLE_DEBUG:
1539 print("Looping through Edges - ", dbg)
1540 dbg = dbg + 1
1542 # Get the end verts on the edge:
1543 v1 = e.verts[0]
1544 v2 = e.verts[1]
1546 # Make sure that verts are not a part of the cutting plane:
1547 if e.select and (v1 not in face.verts and v2 not in face.verts):
1548 if len(face.verts) < 5: # Not an n-gon
1549 intersection = intersect_line_face(e, face, True)
1550 else:
1551 intersection = intersect_line_plane(v1.co, v2.co, face.verts[0].co, normal)
1553 if ENABLE_DEBUG:
1554 print("Intersection: ", intersection)
1556 # If an intersection exists find the distance of each of the end
1557 # points from the plane, with "positive" being in the direction
1558 # of the cutting plane's normal. If the points are on opposite
1559 # side of the plane, then it intersects and we need to cut it
1560 if intersection is not None:
1561 bVerts.ensure_lookup_table()
1562 bEdges.ensure_lookup_table()
1563 bFaces.ensure_lookup_table()
1565 d1 = distance_point_to_plane(v1.co, face.verts[0].co, normal)
1566 d2 = distance_point_to_plane(v2.co, face.verts[0].co, normal)
1567 # If they have different signs, then the edge crosses the cutting plane:
1568 if abs(d1 + d2) < abs(d1 - d2):
1569 # Make the first vertex the positive one:
1570 if d1 < d2:
1571 v2, v1 = v1, v2
1573 if self.make_copy:
1574 new = bVerts.new()
1575 new.co = intersection
1576 new.select = True
1577 bVerts.ensure_lookup_table()
1578 elif self.rip:
1579 if ENABLE_DEBUG:
1580 print("Branch rip engaged")
1581 newV1 = bVerts.new()
1582 newV1.co = intersection
1583 bVerts.ensure_lookup_table()
1584 if ENABLE_DEBUG:
1585 print("newV1 created", end='; ')
1587 newV2 = bVerts.new()
1588 newV2.co = intersection
1589 bVerts.ensure_lookup_table()
1591 if ENABLE_DEBUG:
1592 print("newV2 created", end='; ')
1594 newE1 = bEdges.new((v1, newV1))
1595 newE2 = bEdges.new((v2, newV2))
1596 bEdges.ensure_lookup_table()
1598 if ENABLE_DEBUG:
1599 print("new edges created", end='; ')
1601 if e.is_valid:
1602 bEdges.remove(e)
1604 bEdges.ensure_lookup_table()
1606 if ENABLE_DEBUG:
1607 print("Old edge removed.\nWe're done with this edge")
1608 else:
1609 new = list(bmesh.utils.edge_split(e, v1, 0.5))
1610 bEdges.ensure_lookup_table()
1611 new[1].co = intersection
1612 e.select = False
1613 new[0].select = False
1614 if self.pos:
1615 bEdges.remove(new[0])
1616 if self.neg:
1617 bEdges.remove(e)
1618 bEdges.ensure_lookup_table()
1620 if ENABLE_DEBUG:
1621 print("The Edge Loop has exited. Now to update the bmesh")
1622 dbg = 0
1624 bmesh.update_edit_mesh(me)
1626 except Exception as e:
1627 error_handlers(self, "mesh.edgetools_slice", e,
1628 reports="Slice Operator failed", func=False)
1629 return {'CANCELLED'}
1631 return {'FINISHED'}
1634 # This projects the selected edges onto the selected plane
1635 # and/or both points on the selected edge
1637 class Project(Operator):
1638 bl_idname = "mesh.edgetools_project"
1639 bl_label = "Project"
1640 bl_description = ("Projects the selected Vertices/Edges onto a selected plane\n"
1641 "(Active is projected onto the rest)")
1642 bl_options = {'REGISTER', 'UNDO'}
1644 make_copy: BoolProperty(
1645 name="Make Copy",
1646 description="Make duplicates of the vertices instead of altering them",
1647 default=False
1650 def draw(self, context):
1651 layout = self.layout
1652 layout.prop(self, "make_copy")
1654 @classmethod
1655 def poll(cls, context):
1656 ob = context.active_object
1657 return (ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
1659 def invoke(self, context, event):
1660 return self.execute(context)
1662 def execute(self, context):
1663 try:
1664 me = context.object.data
1665 bm = bmesh.from_edit_mesh(me)
1666 bm.normal_update()
1668 bFaces = bm.faces
1669 bVerts = bm.verts
1671 fVerts = []
1673 # Find the selected face. This will provide the plane to project onto:
1674 # @todo Check first for an active face
1675 for f in bFaces:
1676 if f.select:
1677 for v in f.verts:
1678 fVerts.append(v)
1679 normal = f.normal
1680 f.select = False
1681 break
1683 for v in bVerts:
1684 if v.select:
1685 if v in fVerts:
1686 v.select = False
1687 continue
1688 d = distance_point_to_plane(v.co, fVerts[0].co, normal)
1689 if self.make_copy:
1690 temp = v
1691 v = bVerts.new()
1692 v.co = temp.co
1693 bVerts.ensure_lookup_table()
1694 vector = normal
1695 vector.length = abs(d)
1696 v.co = v.co - (vector * sign(d))
1697 v.select = False
1699 bmesh.update_edit_mesh(me)
1701 except Exception as e:
1702 error_handlers(self, "mesh.edgetools_project", e,
1703 reports="Project Operator failed", func=False)
1705 return {'CANCELLED'}
1707 return {'FINISHED'}
1710 # Project_End is for projecting/extending an edge to meet a plane
1711 # This is used be selecting a face to define the plane then all the edges
1712 # Then move the vertices in the edge that is closest to the
1713 # plane to the coordinates of the intersection of the edge and the plane
1715 class Project_End(Operator):
1716 bl_idname = "mesh.edgetools_project_end"
1717 bl_label = "Project (End Point)"
1718 bl_description = ("Projects the vertices of the selected\n"
1719 "edges closest to a plane onto that plane")
1720 bl_options = {'REGISTER', 'UNDO'}
1722 make_copy: BoolProperty(
1723 name="Make Copy",
1724 description="Make a duplicate of the vertice instead of moving it",
1725 default=False
1727 keep_length: BoolProperty(
1728 name="Keep Edge Length",
1729 description="Maintain edge lengths",
1730 default=False
1732 use_force: BoolProperty(
1733 name="Use opposite vertices",
1734 description="Force the usage of the vertices at the other end of the edge",
1735 default=False
1737 use_normal: BoolProperty(
1738 name="Project along normal",
1739 description="Use the plane's normal as the projection direction",
1740 default=False
1743 def draw(self, context):
1744 layout = self.layout
1746 if not self.keep_length:
1747 layout.prop(self, "use_normal")
1748 layout.prop(self, "make_copy")
1749 layout.prop(self, "use_force")
1751 @classmethod
1752 def poll(cls, context):
1753 ob = context.active_object
1754 return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
1756 def invoke(self, context, event):
1757 return self.execute(context)
1759 def execute(self, context):
1760 try:
1761 me = context.object.data
1762 bm = bmesh.from_edit_mesh(me)
1763 bm.normal_update()
1765 bFaces = bm.faces
1766 bEdges = bm.edges
1767 bVerts = bm.verts
1769 fVerts = []
1771 # Find the selected face. This will provide the plane to project onto:
1772 for f in bFaces:
1773 if f.select:
1774 for v in f.verts:
1775 fVerts.append(v)
1776 normal = f.normal
1777 f.select = False
1778 break
1780 for e in bEdges:
1781 if e.select:
1782 v1 = e.verts[0]
1783 v2 = e.verts[1]
1784 if v1 in fVerts or v2 in fVerts:
1785 e.select = False
1786 continue
1787 intersection = intersect_line_plane(v1.co, v2.co, fVerts[0].co, normal)
1788 if intersection is not None:
1789 # Use abs because we don't care what side of plane we're on:
1790 d1 = distance_point_to_plane(v1.co, fVerts[0].co, normal)
1791 d2 = distance_point_to_plane(v2.co, fVerts[0].co, normal)
1792 # If d1 is closer than we use v1 as our vertice:
1793 # "xor" with 'use_force':
1794 if (abs(d1) < abs(d2)) is not self.use_force:
1795 if self.make_copy:
1796 v1 = bVerts.new()
1797 v1.co = e.verts[0].co
1798 bVerts.ensure_lookup_table()
1799 bEdges.ensure_lookup_table()
1800 if self.keep_length:
1801 v1.co = intersection
1802 elif self.use_normal:
1803 vector = normal
1804 vector.length = abs(d1)
1805 v1.co = v1.co - (vector * sign(d1))
1806 else:
1807 v1.co = intersection
1808 else:
1809 if self.make_copy:
1810 v2 = bVerts.new()
1811 v2.co = e.verts[1].co
1812 bVerts.ensure_lookup_table()
1813 bEdges.ensure_lookup_table()
1814 if self.keep_length:
1815 v2.co = intersection
1816 elif self.use_normal:
1817 vector = normal
1818 vector.length = abs(d2)
1819 v2.co = v2.co - (vector * sign(d2))
1820 else:
1821 v2.co = intersection
1822 e.select = False
1824 bmesh.update_edit_mesh(me)
1826 except Exception as e:
1827 error_handlers(self, "mesh.edgetools_project_end", e,
1828 reports="Project (End Point) Operator failed", func=False)
1829 return {'CANCELLED'}
1831 return {'FINISHED'}
1834 class VIEW3D_MT_edit_mesh_edgetools(Menu):
1835 bl_label = "Edge Tools"
1836 bl_description = "Various tools for manipulating edges"
1838 def draw(self, context):
1839 layout = self.layout
1841 layout.operator("mesh.edgetools_extend")
1842 layout.operator("mesh.edgetools_spline")
1843 layout.operator("mesh.edgetools_ortho")
1844 layout.operator("mesh.edgetools_shaft")
1845 layout.operator("mesh.edgetools_slice")
1846 layout.separator()
1848 layout.operator("mesh.edgetools_project")
1849 layout.operator("mesh.edgetools_project_end")
1852 # define classes for registration
1853 classes = (
1854 VIEW3D_MT_edit_mesh_edgetools,
1855 Extend,
1856 Spline,
1857 Ortho,
1858 Shaft,
1859 Slice,
1860 Project,
1861 Project_End,
1865 # registering and menu integration
1866 def register():
1867 for cls in classes:
1868 bpy.utils.register_class(cls)
1871 # unregistering and removing menus
1872 def unregister():
1873 for cls in classes:
1874 bpy.utils.unregister_class(cls)
1877 if __name__ == "__main__":
1878 register()