3 # This is a toolkit for edge manipulation based on several of mesh manipulation
4 # abilities of several CAD/CAE packages, notably CATIA's Geometric Workbench
5 # from which most of these tools have a functional basis based on the paradims
6 # that platform enables. These tools are a collection of scripts that I needed
7 # at some point, and so I will probably add and improve these as I continue to
8 # use and model with them.
10 # It might be good to eventually merge the tinyCAD VTX tools for unification
11 # purposes, and as these are edge-based tools, it would make sense. Or maybe
12 # merge this with tinyCAD instead?
14 # The GUI and Blender add-on structure shamelessly coded in imitation of the
18 # - "Ortho" inspired from CATIA's line creation tool which creates a line of a
19 # user specified length at a user specified angle to a curve at a chosen
20 # point. The user then selects the plane the line is to be created in.
21 # - "Shaft" is inspired from CATIA's tool of the same name. However, instead
22 # of a curve around an axis, this will instead shaft a line, a point, or
23 # a fixed radius about the selected axis.
24 # - "Slice" is from CATIA's ability to split a curve on a plane. When
25 # completed this be a Python equivalent with all the same basic
26 # functionality, though it will sadly be a little clumsier to use due
27 # to Blender's selection limitations.
30 # - Figure out how to do a GUI for "Shaft", especially for controlling radius?
31 # - Buggy parts have been hidden behind bpy.app.debug. Run Blender in debug
32 # to expose those. Example: Shaft with more than two edges selected.
34 # Paul "BrikBot" Marshall
35 # Created: January 28, 2012
36 # Last Modified: August 25, 2012
37 # Homepage (blog): http://post.darkarsenic.com/
38 # //blog.darkarsenic.com/
40 # Coded in IDLE, tested in Blender 2.63.
41 # Search for "@todo" to quickly find sections that need work.
44 # Functional code comes before fast code. Once it works, then worry about
45 # making it faster/more efficient.
47 # ##### BEGIN GPL LICENSE BLOCK #####
49 # The Blender Edgetools is to bring CAD tools to Blender.
50 # Copyright (C) 2012 Paul Marshall
52 # This program is free software: you can redistribute it and/or modify
53 # it under the terms of the GNU General Public License as published by
54 # the Free Software Foundation, either version 3 of the License, or
55 # (at your option) any later version.
57 # This program is distributed in the hope that it will be useful,
58 # but WITHOUT ANY WARRANTY; without even the implied warranty of
59 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
60 # GNU General Public License for more details.
62 # You should have received a copy of the GNU General Public License
63 # along with this program. If not, see <http://www.gnu.org/licenses/>.
65 # ##### END GPL LICENSE BLOCK #####
72 'author': "Paul Marshall",
75 'location': "View3D > Toolbar and View3D > Specials (W-key)",
77 'description': "CAD style edge manipulation tools",
78 'wiki_url': "http://wiki.blender.org/index.php/Extensions:2.6/Py/Scripts/Modeling/EdgeTools",
79 'tracker_url': "https://blenderpython.svn.sourceforge.net/svnroot/blenderpython/scripts_library/scripts/addons_extern/mesh_edgetools.py",
82 import bpy, bmesh, mathutils
83 from math import acos, pi, radians, sqrt, tan
84 from mathutils import Matrix, Vector
85 from mathutils.geometry import (distance_point_to_plane,
90 from bpy.props import (BoolProperty,
96 # Quick an dirty method for getting the sign of a number:
98 return (number > 0) - (number < 0)
103 # Checks to see if two lines are parallel
104 def is_parallel(v1, v2, v3, v4):
105 result = intersect_line_line(v1, v2, v3, v4)
106 return result == None
111 # This is for the special case where the edge is parallel to an axis. In this
112 # the projection onto the XY plane will fail so it will have to be handled
113 # differently. This tells us if and how:
114 def is_axial(v1, v2, error = 0.000002):
116 # Don't need to store, but is easier to read:
117 vec0 = vector[0] > -error and vector[0] < error
118 vec1 = vector[1] > -error and vector[1] < error
119 vec2 = vector[2] > -error and vector[2] < error
120 if (vec0 or vec1) and vec2:
129 # For some reason "Vector = Vector" does not seem to look at the actual
130 # coordinates. This provides a way to do so.
131 def is_same_co(v1, v2):
132 if len(v1) != len(v2):
135 for co1, co2 in zip(v1, v2):
143 # Tests a face to see if it is planar.
144 def is_face_planar(face, error = 0.0005):
146 d = distance_point_to_plane(v.co, face.verts[0].co, face.normal)
148 print("Distance: " + str(d))
149 if d < -error or d > error:
156 # Starts with an edge. Then scans for linked, selected edges and builds a
157 # list with them in "order", starting at one end and moving towards the other.
158 def order_joined_edges(edge, edges = [], direction = 1):
164 print(edge, end = ", ")
165 print(edges, end = ", ")
166 print(direction, end = "; ")
168 # Robustness check: direction cannot be zero
173 for e in edge.verts[0].link_edges:
174 if e.select and edges.count(e) == 0:
177 newList.extend(order_joined_edges(e, edges, direction + 1))
178 newList.extend(edges)
181 newList.extend(edges)
182 newList.extend(order_joined_edges(e, edges, direction - 1))
184 # This will only matter at the first level:
185 direction = direction * -1
187 for e in edge.verts[1].link_edges:
188 if e.select and edges.count(e) == 0:
191 newList.extend(order_joined_edges(e, edges, direction + 2))
192 newList.extend(edges)
195 newList.extend(edges)
196 newList.extend(order_joined_edges(e, edges, direction))
199 print(newList, end = ", ")
205 # --------------- GEOMETRY CALCULATION METHODS --------------
207 # distance_point_line
209 # I don't know why the mathutils.geometry API does not already have this, but
210 # it is trivial to code using the structures already in place. Instead of
211 # returning a float, I also want to know the direction vector defining the
212 # distance. Distance can be found with "Vector.length".
213 def distance_point_line(pt, line_p1, line_p2):
214 int_co = intersect_point_line(pt, line_p1, line_p2)
215 distance_vector = int_co[0] - pt
216 return distance_vector
219 # interpolate_line_line
221 # This is an experiment into a cubic Hermite spline (c-spline) for connecting
222 # two edges with edges that obey the general equation.
223 # This will return a set of point coordinates (Vectors).
225 # A good, easy to read background on the mathematics can be found at:
226 # http://cubic.org/docs/hermite.htm
228 # Right now this is . . . less than functional :P
230 # - C-Spline and Bezier curves do not end on p2_co as they are supposed to.
231 # - B-Spline just fails. Epically.
232 # - Add more methods as I come across them. Who said flexibility was bad?
233 def interpolate_line_line(p1_co, p1_dir, p2_co, p2_dir, segments, tension = 1,
234 typ = 'BEZIER', include_ends = False):
236 fraction = 1 / segments
237 # Form: p1, tangent 1, p2, tangent 2
239 poly = [[2, -3, 0, 1], [1, -2, 1, 0],
240 [-2, 3, 0, 0], [1, -1, 0, 0]]
241 elif typ == 'BEZIER':
242 poly = [[-1, 3, -3, 1], [3, -6, 3, 0],
243 [1, 0, 0, 0], [-3, 3, 0, 0]]
244 p1_dir = p1_dir + p1_co
245 p2_dir = -p2_dir + p2_co
246 elif typ == 'BSPLINE':
247 ## Supposed poly matrix for a cubic b-spline:
248 ## poly = [[-1, 3, -3, 1], [3, -6, 3, 0],
249 ## [-3, 0, 3, 0], [1, 4, 1, 0]]
250 # My own invention to try to get something that somewhat acts right.
251 # This is semi-quadratic rather than fully cubic:
252 poly = [[0, -1, 0, 1], [1, -2, 1, 0],
253 [0, -1, 2, 0], [1, -1, 0, 0]]
256 # Generate each point:
257 for i in range(segments - 1):
258 t = fraction * (i + 1)
261 s = [t ** 3, t ** 2, t, 1]
262 h00 = (poly[0][0] * s[0]) + (poly[0][1] * s[1]) + (poly[0][2] * s[2]) + (poly[0][3] * s[3])
263 h01 = (poly[1][0] * s[0]) + (poly[1][1] * s[1]) + (poly[1][2] * s[2]) + (poly[1][3] * s[3])
264 h10 = (poly[2][0] * s[0]) + (poly[2][1] * s[1]) + (poly[2][2] * s[2]) + (poly[2][3] * s[3])
265 h11 = (poly[3][0] * s[0]) + (poly[3][1] * s[1]) + (poly[3][2] * s[2]) + (poly[3][3] * s[3])
266 pieces.append((h00 * p1_co) + (h01 * p1_dir) + (h10 * p2_co) + (h11 * p2_dir))
278 # intersect_line_face
280 # Calculates the coordinate of intersection of a line with a face. It returns
281 # the coordinate if one exists, otherwise None. It can only deal with tris or
282 # quads for a face. A quad does NOT have to be planar. Thus the following.
284 # Quad math and theory:
285 # A quad may not be planar. Therefore the treated definition of the surface is
286 # that the surface is composed of all lines bridging two other lines defined by
287 # the given four points. The lines do not "cross".
289 # The two lines in 3-space can defined as:
290 # ┌ ┐ ┌ ┐ ┌ ┐ ┌ ┐ ┌ ┐ ┌ ┐
291 # │x1│ │a11│ │b11│ │x2│ │a21│ │b21│
292 # │y1│ = (1-t1)│a12│ + t1│b12│, │y2│ = (1-t2)│a22│ + t2│b22│
293 # │z1│ │a13│ │b13│ │z2│ │a23│ │b23│
294 # └ ┘ └ ┘ └ ┘ └ ┘ └ ┘ └ ┘
295 # Therefore, the surface is the lines defined by every point alone the two
296 # lines with a same "t" value (t1 = t2). This is basically R = V1 + tQ, where
297 # Q = V2 - V1 therefore R = V1 + t(V2 - V1) -> R = (1 - t)V1 + tV2:
299 # │x12│ │(1-t)a11 + t * b11│ │(1-t)a21 + t * b21│
300 # │y12│ = (1 - t12)│(1-t)a12 + t * b12│ + t12│(1-t)a22 + t * b22│
301 # │z12│ │(1-t)a13 + t * b13│ │(1-t)a23 + t * b23│
303 # Now, the equation of our line can be likewise defined:
306 # │y3│ = │a32│ + t3│b32│
309 # Now we just have to find a valid solution for the two equations. This should
310 # be our point of intersection. Therefore, x12 = x3 -> x, y12 = y3 -> y,
311 # z12 = z3 -> z. Thus, to find that point we set the equation defining the
312 # surface as equal to the equation for the line:
314 # │(1-t)a11 + t * b11│ │(1-t)a21 + t * b21│ │a31│ │b31│
315 # (1 - t12)│(1-t)a12 + t * b12│ + t12│(1-t)a22 + t * b22│ = │a32│ + t3│b32│
316 # │(1-t)a13 + t * b13│ │(1-t)a23 + t * b23│ │a33│ │b33│
318 # This leaves us with three equations, three unknowns. Solving the system by
319 # hand is practically impossible, but using Mathematica we are given an insane
320 # series of three equations (not reproduced here for the sake of space: see
321 # http://www.mediafire.com/file/cc6m6ba3sz2b96m/intersect_line_surface.nb and
322 # http://www.mediafire.com/file/0egbr5ahg14talm/intersect_line_surface2.nb for
323 # Mathematica computation).
325 # Additionally, the resulting series of equations may result in a div by zero
326 # exception if the line in question if parallel to one of the axis or if the
327 # quad is planar and parallel to either the XY, XZ, or YZ planes. However, the
328 # system is still solvable but must be dealt with a little differently to avaid
329 # these special cases. Because the resulting equations are a little different,
330 # we have to code them differently. Hence the special cases.
332 # Tri math and theory:
333 # A triangle must be planar (three points define a plane). Therefore we just
334 # have to make sure that the line intersects inside the triangle.
336 # If the point is within the triangle, then the angle between the lines that
337 # connect the point to the each individual point of the triangle will be
338 # equal to 2 * PI. Otherwise, if the point is outside the triangle, then the
339 # sum of the angles will be less.
342 # - Figure out how to deal with n-gons. How the heck is a face with 8 verts
343 # definied mathematically? How do I then find the intersection point of
344 # a line with said vert? How do I know if that point is "inside" all the
345 # verts? I have no clue, and haven't been able to find anything on it so
346 # far. Maybe if someone (actually reads this and) who knows could note?
347 def intersect_line_face(edge, face, is_infinite = False, error = 0.000002):
350 # If we are dealing with a non-planar quad:
351 if len(face.verts) == 4 and not is_face_planar(face):
352 edgeA = face.edges[0]
356 for i in range(len(face.edges)):
357 if face.edges[i].verts[0] not in edgeA.verts and face.edges[i].verts[1] not in edgeA.verts:
358 edgeB = face.edges[i]
361 # I haven't figured out a way to mix this in with the above. Doing so might remove a
362 # few extra instructions from having to be executed saving a few clock cycles:
363 for i in range(len(face.edges)):
364 if face.edges[i] == edgeA or face.edges[i] == edgeB:
366 if (edgeA.verts[0] in face.edges[i].verts and edgeB.verts[1] in face.edges[i].verts) or (edgeA.verts[1] in face.edges[i].verts and edgeB.verts[0] in face.edges[i].verts):
370 # Define calculation coefficient constants:
371 # "xx1" is the x coordinate, "xx2" is the y coordinate, and "xx3" is the z
373 a11, a12, a13 = edgeA.verts[0].co[0], edgeA.verts[0].co[1], edgeA.verts[0].co[2]
374 b11, b12, b13 = edgeA.verts[1].co[0], edgeA.verts[1].co[1], edgeA.verts[1].co[2]
376 a21, a22, a23 = edgeB.verts[1].co[0], edgeB.verts[1].co[1], edgeB.verts[1].co[2]
377 b21, b22, b23 = edgeB.verts[0].co[0], edgeB.verts[0].co[1], edgeB.verts[0].co[2]
379 a21, a22, a23 = edgeB.verts[0].co[0], edgeB.verts[0].co[1], edgeB.verts[0].co[2]
380 b21, b22, b23 = edgeB.verts[1].co[0], edgeB.verts[1].co[1], edgeB.verts[1].co[2]
381 a31, a32, a33 = edge.verts[0].co[0], edge.verts[0].co[1], edge.verts[0].co[2]
382 b31, b32, b33 = edge.verts[1].co[0], edge.verts[1].co[1], edge.verts[1].co[2]
384 # There are a bunch of duplicate "sub-calculations" inside the resulting
385 # equations for t, t12, and t3. Calculate them once and store them to
386 # reduce computational time:
387 m01 = a13 * a22 * a31
388 m02 = a12 * a23 * a31
389 m03 = a13 * a21 * a32
390 m04 = a11 * a23 * a32
391 m05 = a12 * a21 * a33
392 m06 = a11 * a22 * a33
393 m07 = a23 * a32 * b11
394 m08 = a22 * a33 * b11
395 m09 = a23 * a31 * b12
396 m10 = a21 * a33 * b12
397 m11 = a22 * a31 * b13
398 m12 = a21 * a32 * b13
399 m13 = a13 * a32 * b21
400 m14 = a12 * a33 * b21
401 m15 = a13 * a31 * b22
402 m16 = a11 * a33 * b22
403 m17 = a12 * a31 * b23
404 m18 = a11 * a32 * b23
405 m19 = a13 * a22 * b31
406 m20 = a12 * a23 * b31
407 m21 = a13 * a32 * b31
408 m22 = a23 * a32 * b31
409 m23 = a12 * a33 * b31
410 m24 = a22 * a33 * b31
411 m25 = a23 * b12 * b31
412 m26 = a33 * b12 * b31
413 m27 = a22 * b13 * b31
414 m28 = a32 * b13 * b31
415 m29 = a13 * b22 * b31
416 m30 = a33 * b22 * b31
417 m31 = a12 * b23 * b31
418 m32 = a32 * b23 * b31
419 m33 = a13 * a21 * b32
420 m34 = a11 * a23 * b32
421 m35 = a13 * a31 * b32
422 m36 = a23 * a31 * b32
423 m37 = a11 * a33 * b32
424 m38 = a21 * a33 * b32
425 m39 = a23 * b11 * b32
426 m40 = a33 * b11 * b32
427 m41 = a21 * b13 * b32
428 m42 = a31 * b13 * b32
429 m43 = a13 * b21 * b32
430 m44 = a33 * b21 * b32
431 m45 = a11 * b23 * b32
432 m46 = a31 * b23 * b32
433 m47 = a12 * a21 * b33
434 m48 = a11 * a22 * b33
435 m49 = a12 * a31 * b33
436 m50 = a22 * a31 * b33
437 m51 = a11 * a32 * b33
438 m52 = a21 * a32 * b33
439 m53 = a22 * b11 * b33
440 m54 = a32 * b11 * b33
441 m55 = a21 * b12 * b33
442 m56 = a31 * b12 * b33
443 m57 = a12 * b21 * b33
444 m58 = a32 * b21 * b33
445 m59 = a11 * b22 * b33
446 m60 = a31 * b22 * b33
447 m61 = a33 * b12 * b21
448 m62 = a32 * b13 * b21
449 m63 = a33 * b11 * b22
450 m64 = a31 * b13 * b22
451 m65 = a32 * b11 * b23
452 m66 = a31 * b12 * b23
453 m67 = b13 * b22 * b31
454 m68 = b12 * b23 * b31
455 m69 = b13 * b21 * b32
456 m70 = b11 * b23 * b32
457 m71 = b12 * b21 * b33
458 m72 = b11 * b22 * b33
459 n01 = m01 - m02 - m03 + m04 + m05 - m06
460 n02 = -m07 + m08 + m09 - m10 - m11 + m12 + m13 - m14 - m15 + m16 + m17 - m18 - m25 + m27 + m29 - m31 + m39 - m41 - m43 + m45 - m53 + m55 + m57 - m59
461 n03 = -m19 + m20 + m33 - m34 - m47 + m48
462 n04 = m21 - m22 - m23 + m24 - m35 + m36 + m37 - m38 + m49 - m50 - m51 + m52
463 n05 = m26 - m28 - m30 + m32 - m40 + m42 + m44 - m46 + m54 - m56 - m58 + m60
464 n06 = m61 - m62 - m63 + m64 + m65 - m66 - m67 + m68 + m69 - m70 - m71 + m72
465 n07 = 2 * n01 + n02 + 2 * n03 + n04 + n05
466 n08 = n01 + n02 + n03 + n06
468 # Calculate t, t12, and t3:
469 t = (n07 - sqrt(pow(-n07, 2) - 4 * (n01 + n03 + n04) * n08)) / (2 * n08)
471 # t12 can be greatly simplified by defining it with t in it:
472 # If block used to help prevent any div by zero error.
476 # The line is parallel to the z-axis:
478 t12 = ((a11 - a31) + (b11 - a11) * t) / ((a21 - a11) + (a11 - a21 - b11 + b21) * t)
479 # The line is parallel to the y-axis:
481 t12 = ((a11 - a31) + (b11 - a11) * t) / ((a21 - a11) + (a11 - a21 - b11 + b21) * t)
482 # The line is along the y/z-axis but is not parallel to either:
484 t12 = -(-(a33 - b33) * (-a32 + a12 * (1 - t) + b12 * t) + (a32 - b32) * (-a33 + a13 * (1 - t) + b13 * t)) / (-(a33 - b33) * ((a22 - a12) * (1 - t) + (b22 - b12) * t) + (a32 - b32) * ((a23 - a13) * (1 - t) + (b23 - b13) * t))
486 # The line is parallel to the x-axis:
488 t12 = ((a12 - a32) + (b12 - a12) * t) / ((a22 - a12) + (a12 - a22 - b12 + b22) * t)
489 # The line is along the x/z-axis but is not parallel to either:
491 t12 = -(-(a33 - b33) * (-a31 + a11 * (1 - t) + b11 * t) + (a31 - b31) * (-a33 + a13 * (1 - t) + b13 * t)) / (-(a33 - b33) * ((a21 - a11) * (1 - t) + (b21 - b11) * t) + (a31 - b31) * ((a23 - a13) * (1 - t) + (b23 - b13) * t))
492 # The line is along the x/y-axis but is not parallel to either:
494 t12 = -(-(a32 - b32) * (-a31 + a11 * (1 - t) + b11 * t) + (a31 - b31) * (-a32 + a12 * (1 - t) + b12 * t)) / (-(a32 - b32) * ((a21 - a11) * (1 - t) + (b21 - b11) * t) + (a31 - b31) * ((a22 - a21) * (1 - t) + (b22 - b12) * t))
496 # Likewise, t3 is greatly simplified by defining it in terms of t and t12:
497 # If block used to prevent a div by zero error.
500 t3 = (-a11 + a31 + (a11 - b11) * t + (a11 - a21) * t12 + (a21 - a11 + b11 - b21) * t * t12) / (a31 - b31)
502 t3 = (-a12 + a32 + (a12 - b12) * t + (a12 - a22) * t12 + (a22 - a12 + b12 - b22) * t * t12) / (a32 - b32)
504 t3 = (-a13 + a33 + (a13 - b13) * t + (a13 - a23) * t12 + (a23 - a13 + b13 - b23) * t * t12) / (a33 - b33)
506 print("The second edge is a zero-length edge")
509 # Calculate the point of intersection:
510 x = (1 - t3) * a31 + t3 * b31
511 y = (1 - t3) * a32 + t3 * b32
512 z = (1 - t3) * a33 + t3 * b33
513 int_co = Vector((x, y, z))
518 # If the line does not intersect the quad, we return "None":
519 if (t < -1 or t > 1 or t12 < -1 or t12 > 1) and not is_infinite:
522 elif len(face.verts) == 3:
523 p1, p2, p3 = face.verts[0].co, face.verts[1].co, face.verts[2].co
524 int_co = intersect_line_plane(edge.verts[0].co, edge.verts[1].co, p1, face.normal)
526 # Only check if the triangle is not being treated as an infinite plane:
527 if int_co != None and not is_infinite:
531 aAB = acos(pA.dot(pB))
532 aBC = acos(pB.dot(pC))
533 aCA = acos(pC.dot(pA))
534 sumA = aAB + aBC + aCA
536 # If the point is outside the triangle:
537 if (sumA > (pi + error) and sumA < (pi - error)):
540 # This is the default case where we either have a planar quad or an n-gon.
542 int_co = intersect_line_plane(edge.verts[0].co, edge.verts[1].co,
543 face.verts[0].co, face.normal)
548 # project_point_plane
550 # Projects a point onto a plane. Returns a tuple of the projection vector
551 # and the projected coordinate.
552 def project_point_plane(pt, plane_co, plane_no):
553 proj_co = intersect_line_plane(pt, pt + plane_no, plane_co, plane_no)
554 proj_ve = proj_co - pt
555 return (proj_ve, proj_co)
558 # ------------ FILLET/CHAMPHER HELPER METHODS -------------
562 # The following is used to return edges that might be possible edges for
563 # propagation. If an edge is connected to the end vert, but is also a part
564 # of the on of the faces that the current edge composes, then it is a
565 # "corner edge" and is not valid as a propagation edge. If the edge is
566 # part of two faces that a in the same plane, then we cannot fillet/chamfer
567 # it because there is no angle between them.
568 def get_next_edge(edge, vert):
569 invalidEdges = [e for f in edge.link_faces for e in f.edges if e != edge]
570 invalidEdges.append(edge)
573 newEdge = [e for e in vert.link_edges if e not in invalidEdges and not is_planar_edge(e)]
574 if len(newEdge) == 0:
576 elif len(newEdge) == 1:
582 def is_planar_edge(edge, error = 0.000002):
583 angle = edge.calc_face_angle()
584 return (angle < error and angle > -error) or (angle < (180 + error) and angle > (180 - error))
589 # Calculates the base geometry data for the fillet. This assumes that the faces
593 # - Redesign so that the faces do not have to be planar
595 # There seems to be issues some of the vector math right now. Will need to be
597 def fillet_axis(edge, radius):
598 vectors = [None, None, None, None]
600 origin = Vector((0, 0, 0))
601 axis = edge.verts[1].co - edge.verts[0].co
603 # Get the "adjacency" base vectors for face 0:
604 for e in edge.link_faces[0].edges:
607 if e.verts[0] == edge.verts[0]:
608 vectors[0] = e.verts[1].co - e.verts[0].co
609 elif e.verts[1] == edge.verts[0]:
610 vectors[0] = e.verts[0].co - e.verts[1].co
611 elif e.verts[0] == edge.verts[1]:
612 vectors[1] = e.verts[1].co - e.verts[0].co
613 elif e.verts[1] == edge.verts[1]:
614 vectors[1] = e.verts[0].co - e.verts[1].co
616 # Get the "adjacency" base vectors for face 1:
617 for e in edge.link_faces[1].edges:
620 if e.verts[0] == edge.verts[0]:
621 vectors[2] = e.verts[1].co - e.verts[0].co
622 elif e.verts[1] == edge.verts[0]:
623 vectors[2] = e.verts[0].co - e.verts[1].co
624 elif e.verts[0] == edge.verts[1]:
625 vectors[3] = e.verts[1].co - e.verts[0].co
626 elif e.verts[1] == edge.verts[1]:
627 vectors[3] = e.verts[0].co - e.verts[1].co
629 # Get the normal for face 0 and face 1:
630 norm1 = edge.link_faces[0].normal
631 norm2 = edge.link_faces[1].normal
633 # We need to find the angle between the two faces, then bisect it:
634 theda = (pi - edge.calc_face_angle()) / 2
636 # We are dealing with a triangle here, and we will need the length
637 # of its adjacent side. The opposite is the radius:
638 adj_len = radius / tan(theda)
640 # Vectors can be thought of as being at the origin, and we need to make sure
641 # that the base vectors are planar with the "normal" definied by the edge to
642 # be filleted. Then we set the length of the vector and shift it into a
644 for i in range(len(vectors)):
645 vectors[i] = project_point_plane(vectors[i], origin, axis)[1]
646 vectors[i].length = adj_len
647 vectors[i] = vectors[i] + edge.verts[i % 2].co
649 # Compute fillet axis end points:
650 v1 = intersect_line_line(vectors[0], vectors[0] + norm1, vectors[2], vectors[2] + norm2)[0]
651 v2 = intersect_line_line(vectors[1], vectors[1] + norm1, vectors[3], vectors[3] + norm2)[0]
655 def fillet_point(t, face1, face2):
659 # ------------------- EDGE TOOL METHODS -------------------
661 # Extends an "edge" in two directions:
662 # - Requires two vertices to be selected. They do not have to form an edge.
663 # - Extends "length" in both directions
664 class Extend(bpy.types.Operator):
665 bl_idname = "mesh.edgetools_extend"
667 bl_description = "Extend the selected edges of vertice pair."
668 bl_options = {'REGISTER', 'UNDO'}
670 di1 = BoolProperty(name = "Forwards",
671 description = "Extend the edge forwards",
673 di2 = BoolProperty(name = "Backwards",
674 description = "Extend the edge backwards",
676 length = FloatProperty(name = "Length",
677 description = "Length to extend the edge",
678 min = 0.0, max = 1024.0,
681 def draw(self, context):
683 layout.prop(self, "di1")
684 layout.prop(self, "di2")
685 layout.prop(self, "length")
689 def poll(cls, context):
690 ob = context.active_object
691 return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
694 def invoke(self, context, event):
695 return self.execute(context)
698 def execute(self, context):
699 bpy.ops.object.editmode_toggle()
701 bm.from_mesh(bpy.context.active_object.data)
707 edges = [e for e in bEdges if e.select]
708 verts = [v for v in bVerts if v.select]
712 vector = e.verts[0].co - e.verts[1].co
713 vector.length = self.length
717 if (vector[0] + vector[1] + vector[2]) < 0:
718 v.co = e.verts[1].co - vector
719 newE = bEdges.new((e.verts[1], v))
721 v.co = e.verts[0].co + vector
722 newE = bEdges.new((e.verts[0], v))
725 if (vector[0] + vector[1] + vector[2]) < 0:
726 v.co = e.verts[0].co + vector
727 newE = bEdges.new((e.verts[0], v))
729 v.co = e.verts[1].co - vector
730 newE = bEdges.new((e.verts[1], v))
732 vector = verts[0].co - verts[1].co
733 vector.length = self.length
737 if (vector[0] + vector[1] + vector[2]) < 0:
738 v.co = verts[1].co - vector
739 e = bEdges.new((verts[1], v))
741 v.co = verts[0].co + vector
742 e = bEdges.new((verts[0], v))
745 if (vector[0] + vector[1] + vector[2]) < 0:
746 v.co = verts[0].co + vector
747 e = bEdges.new((verts[0], v))
749 v.co = verts[1].co - vector
750 e = bEdges.new((verts[1], v))
752 bm.to_mesh(bpy.context.active_object.data)
753 bpy.ops.object.editmode_toggle()
757 # Creates a series of edges between two edges using spline interpolation.
758 # This basically just exposes existing functionality in addition to some
759 # other common methods: Hermite (c-spline), Bezier, and b-spline. These
760 # alternates I coded myself after some extensive research into spline
763 # @todo Figure out what's wrong with the Blender bezier interpolation.
764 class Spline(bpy.types.Operator):
765 bl_idname = "mesh.edgetools_spline"
767 bl_description = "Create a spline interplopation between two edges"
768 bl_options = {'REGISTER', 'UNDO'}
770 alg = EnumProperty(name = "Spline Algorithm",
771 items = [('Blender', 'Blender', 'Interpolation provided through \"mathutils.geometry\"'),
772 ('Hermite', 'C-Spline', 'C-spline interpolation'),
773 ('Bezier', 'Bézier', 'Bézier interpolation'),
774 ('B-Spline', 'B-Spline', 'B-Spline interpolation')],
776 segments = IntProperty(name = "Segments",
777 description = "Number of segments to use in the interpolation",
781 flip1 = BoolProperty(name = "Flip Edge",
782 description = "Flip the direction of the spline on edge 1",
784 flip2 = BoolProperty(name = "Flip Edge",
785 description = "Flip the direction of the spline on edge 2",
787 ten1 = FloatProperty(name = "Tension",
788 description = "Tension on edge 1",
789 min = -4096.0, max = 4096.0,
790 soft_min = -8.0, soft_max = 8.0,
792 ten2 = FloatProperty(name = "Tension",
793 description = "Tension on edge 2",
794 min = -4096.0, max = 4096.0,
795 soft_min = -8.0, soft_max = 8.0,
798 def draw(self, context):
801 layout.prop(self, "alg")
802 layout.prop(self, "segments")
803 layout.label("Edge 1:")
804 layout.prop(self, "ten1")
805 layout.prop(self, "flip1")
806 layout.label("Edge 2:")
807 layout.prop(self, "ten2")
808 layout.prop(self, "flip2")
812 def poll(cls, context):
813 ob = context.active_object
814 return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
817 def invoke(self, context, event):
818 return self.execute(context)
821 def execute(self, context):
822 bpy.ops.object.editmode_toggle()
824 bm.from_mesh(bpy.context.active_object.data)
831 edges = [e for e in bEdges if e.select]
832 verts = [edges[v // 2].verts[v % 2] for v in range(4)]
837 p1_dir = verts[1].co - verts[0].co
841 p1_dir = verts[0].co - verts[1].co
844 p1_dir.length = -self.ten1
846 p1_dir.length = self.ten1
851 p2_dir = verts[2].co - verts[3].co
855 p2_dir = verts[3].co - verts[2].co
858 p2_dir.length = -self.ten2
860 p2_dir.length = self.ten2
862 # Get the interploted coordinates:
863 if self.alg == 'Blender':
864 pieces = interpolate_bezier(p1_co, p1_dir, p2_dir, p2_co, self.segments)
865 elif self.alg == 'Hermite':
866 pieces = interpolate_line_line(p1_co, p1_dir, p2_co, p2_dir, self.segments, 1, 'HERMITE')
867 elif self.alg == 'Bezier':
868 pieces = interpolate_line_line(p1_co, p1_dir, p2_co, p2_dir, self.segments, 1, 'BEZIER')
869 elif self.alg == 'B-Spline':
870 pieces = interpolate_line_line(p1_co, p1_dir, p2_co, p2_dir, self.segments, 1, 'BSPLINE')
874 # Add vertices and set the points:
875 for i in range(seg - 1):
882 e = bEdges.new((verts[i], verts[i + 1]))
884 bm.to_mesh(bpy.context.active_object.data)
885 bpy.ops.object.editmode_toggle()
889 # Creates edges normal to planes defined between each of two edges and the
890 # normal or the plane defined by those two edges.
891 # - Select two edges. The must form a plane.
892 # - On running the script, eight edges will be created. Delete the
893 # extras that you don't need.
894 # - The length of those edges is defined by the variable "length"
896 # @todo Change method from a cross product to a rotation matrix to make the
898 # --- todo completed Feb 4th, but still needs work ---
899 # @todo Figure out a way to make +/- predictable
900 # - Maybe use angel between edges and vector direction definition?
901 # --- TODO COMPLETED ON 2/9/2012 ---
902 class Ortho(bpy.types.Operator):
903 bl_idname = "mesh.edgetools_ortho"
904 bl_label = "Angle Off Edge"
906 bl_options = {'REGISTER', 'UNDO'}
908 vert1 = BoolProperty(name = "Vertice 1",
909 description = "Enable edge creation for vertice 1.",
911 vert2 = BoolProperty(name = "Vertice 2",
912 description = "Enable edge creation for vertice 2.",
914 vert3 = BoolProperty(name = "Vertice 3",
915 description = "Enable edge creation for vertice 3.",
917 vert4 = BoolProperty(name = "Vertice 4",
918 description = "Enable edge creation for vertice 4.",
920 pos = BoolProperty(name = "+",
921 description = "Enable positive direction edges.",
923 neg = BoolProperty(name = "-",
924 description = "Enable negitive direction edges.",
926 angle = FloatProperty(name = "Angle",
927 description = "Angle off of the originating edge",
928 min = 0.0, max = 180.0,
930 length = FloatProperty(name = "Length",
931 description = "Length of created edges.",
932 min = 0.0, max = 1024.0,
935 # For when only one edge is selected (Possible feature to be testd):
936 plane = EnumProperty(name = "Plane",
937 items = [("XY", "X-Y Plane", "Use the X-Y plane as the plane of creation"),
938 ("XZ", "X-Z Plane", "Use the X-Z plane as the plane of creation"),
939 ("YZ", "Y-Z Plane", "Use the Y-Z plane as the plane of creation")],
942 def draw(self, context):
945 layout.prop(self, "vert1")
946 layout.prop(self, "vert2")
947 layout.prop(self, "vert3")
948 layout.prop(self, "vert4")
949 row = layout.row(align = False)
950 row.alignment = 'EXPAND'
951 row.prop(self, "pos")
952 row.prop(self, "neg")
953 layout.prop(self, "angle")
954 layout.prop(self, "length")
957 def poll(cls, context):
958 ob = context.active_object
959 return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
962 def invoke(self, context, event):
963 return self.execute(context)
966 def execute(self, context):
967 bpy.ops.object.editmode_toggle()
969 bm.from_mesh(bpy.context.active_object.data)
974 edges = [e for e in bEdges if e.select]
977 # Until I can figure out a better way of handeling it:
979 bpy.ops.object.editmode_toggle()
980 self.report({'ERROR_INVALID_INPUT'},
981 "You must select two edges.")
984 verts = [edges[0].verts[0],
989 cos = intersect_line_line(verts[0].co, verts[1].co, verts[2].co, verts[3].co)
991 # If the two edges are parallel:
993 self.report({'WARNING'},
994 "Selected lines are parallel: results may be unpredictable.")
995 vectors.append(verts[0].co - verts[1].co)
996 vectors.append(verts[0].co - verts[2].co)
997 vectors.append(vectors[0].cross(vectors[1]))
998 vectors.append(vectors[2].cross(vectors[0]))
999 vectors.append(-vectors[3])
1001 # Warn the user if they have not chosen two planar edges:
1002 if not is_same_co(cos[0], cos[1]):
1003 self.report({'WARNING'},
1004 "Selected lines are not planar: results may be unpredictable.")
1006 # This makes the +/- behavior predictable:
1007 if (verts[0].co - cos[0]).length < (verts[1].co - cos[0]).length:
1008 verts[0], verts[1] = verts[1], verts[0]
1009 if (verts[2].co - cos[0]).length < (verts[3].co - cos[0]).length:
1010 verts[2], verts[3] = verts[3], verts[2]
1012 vectors.append(verts[0].co - verts[1].co)
1013 vectors.append(verts[2].co - verts[3].co)
1015 # Normal of the plane formed by vector1 and vector2:
1016 vectors.append(vectors[0].cross(vectors[1]))
1018 # Possible directions:
1019 vectors.append(vectors[2].cross(vectors[0]))
1020 vectors.append(vectors[1].cross(vectors[2]))
1023 vectors[3].length = self.length
1024 vectors[4].length = self.length
1026 # Perform any additional rotations:
1027 matrix = Matrix.Rotation(radians(90 + self.angle), 3, vectors[2])
1028 vectors.append(matrix * -vectors[3]) # vectors[5]
1029 matrix = Matrix.Rotation(radians(90 - self.angle), 3, vectors[2])
1030 vectors.append(matrix * vectors[4]) # vectors[6]
1031 vectors.append(matrix * vectors[3]) # vectors[7]
1032 matrix = Matrix.Rotation(radians(90 + self.angle), 3, vectors[2])
1033 vectors.append(matrix * -vectors[4]) # vectors[8]
1035 # Perform extrusions and displacements:
1036 # There will be a total of 8 extrusions. One for each vert of each edge.
1037 # It looks like an extrusion will add the new vert to the end of the verts
1038 # list and leave the rest in the same location.
1039 # ----------- EDIT -----------
1040 # It looks like I might be able to do this within "bpy.data" with the ".add"
1042 # ------- BMESH UPDATE -------
1043 # BMesh uses ".new()"
1045 for v in range(len(verts)):
1047 if (v == 0 and self.vert1) or (v == 1 and self.vert2) or (v == 2 and self.vert3) or (v == 3 and self.vert4):
1050 new.co = vert.co - vectors[5 + (v // 2) + ((v % 2) * 2)]
1051 bEdges.new((vert, new))
1054 new.co = vert.co + vectors[5 + (v // 2) + ((v % 2) * 2)]
1055 bEdges.new((vert, new))
1057 bm.to_mesh(bpy.context.active_object.data)
1058 bpy.ops.object.editmode_toggle()
1063 # Select an edge and a point or an edge and specify the radius (default is 1 BU)
1064 # You can select two edges but it might be unpredicatble which edge it revolves
1065 # around so you might have to play with the switch.
1066 class Shaft(bpy.types.Operator):
1067 bl_idname = "mesh.edgetools_shaft"
1069 bl_description = "Create a shaft mesh around an axis"
1070 bl_options = {'REGISTER', 'UNDO'}
1072 # Selection defaults:
1075 # For tracking if the user has changed selection:
1076 last_edge = IntProperty(name = "Last Edge",
1077 description = "Tracks if user has changed selected edge",
1082 edge = IntProperty(name = "Edge",
1083 description = "Edge to shaft around.",
1086 flip = BoolProperty(name = "Flip Second Edge",
1087 description = "Flip the percieved direction of the second edge.",
1089 radius = FloatProperty(name = "Radius",
1090 description = "Shaft Radius",
1091 min = 0.0, max = 1024.0,
1093 start = FloatProperty(name = "Starting Angle",
1094 description = "Angle to start the shaft at.",
1095 min = -360.0, max = 360.0,
1097 finish = FloatProperty(name = "Ending Angle",
1098 description = "Angle to end the shaft at.",
1099 min = -360.0, max = 360.0,
1101 segments = IntProperty(name = "Shaft Segments",
1102 description = "Number of sgements to use in the shaft.",
1103 min = 1, max = 4096,
1108 def draw(self, context):
1109 layout = self.layout
1111 if self.shaftType == 0:
1112 layout.prop(self, "edge")
1113 layout.prop(self, "flip")
1114 elif self.shaftType == 3:
1115 layout.prop(self, "radius")
1116 layout.prop(self, "segments")
1117 layout.prop(self, "start")
1118 layout.prop(self, "finish")
1122 def poll(cls, context):
1123 ob = context.active_object
1124 return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
1127 def invoke(self, context, event):
1128 # Make sure these get reset each time we run:
1132 return self.execute(context)
1135 def execute(self, context):
1136 bpy.ops.object.editmode_toggle()
1138 bm.from_mesh(bpy.context.active_object.data)
1149 # Pre-caclulated values:
1151 rotRange = [radians(self.start), radians(self.finish)]
1152 rads = radians((self.finish - self.start) / self.segments)
1154 numV = self.segments + 1
1155 numE = self.segments
1157 edges = [e for e in bEdges if e.select]
1159 # Robustness check: there should at least be one edge selected
1161 bpy.ops.object.editmode_toggle()
1162 self.report({'ERROR_INVALID_INPUT'},
1163 "At least one edge must be selected.")
1164 return {'CANCELLED'}
1166 # If two edges are selected:
1174 # By default, we want to shaft around the last selected edge (it
1175 # will be the active edge). We know we are using the default if
1176 # the user has not changed which edge is being shafted around (as
1177 # is tracked by self.last_edge). When they are not the same, then
1178 # the user has changed selection.
1180 # We then need to make sure that the active object really is an edge
1181 # (robustness check).
1183 # Finally, if the active edge is not the inital one, we flip them
1184 # and have the GUI reflect that.
1185 if self.last_edge == self.edge:
1186 if isinstance(bm.select_history.active, bmesh.types.BMEdge):
1187 if bm.select_history.active != edges[edge[0]]:
1188 self.last_edge, self.edge = edge[1], edge[1]
1189 edge = [edge[1], edge[0]]
1191 bpy.ops.object.editmode_toggle()
1192 self.report({'ERROR_INVALID_INPUT'},
1193 "Active geometry is not an edge.")
1194 return {'CANCELLED'}
1195 elif self.edge == 1:
1198 verts.append(edges[edge[0]].verts[0])
1199 verts.append(edges[edge[0]].verts[1])
1204 verts.append(edges[edge[1]].verts[vert[0]])
1205 verts.append(edges[edge[1]].verts[vert[1]])
1208 # If there is more than one edge selected:
1209 # There are some issues with it ATM, so don't expose is it to normal users:
1210 elif len(edges) > 2 and bpy.app.debug:
1211 if isinstance(bm.select_history.active, bmesh.types.BMEdge):
1212 active = bm.select_history.active
1213 edges.remove(active)
1214 # Get all the verts:
1215 edges = order_joined_edges(edges[0])
1218 if verts.count(e.verts[0]) == 0:
1219 verts.append(e.verts[0])
1220 if verts.count(e.verts[1]) == 0:
1221 verts.append(e.verts[1])
1223 bpy.ops.object.editmode_toggle()
1224 self.report({'ERROR_INVALID_INPUT'},
1225 "Active geometry is not an edge.")
1226 return {'CANCELLED'}
1229 verts.append(edges[0].verts[0])
1230 verts.append(edges[0].verts[1])
1233 if v.select and verts.count(v) == 0:
1241 # The vector denoting the axis of rotation:
1242 if self.shaftType == 1:
1243 axis = active.verts[1].co - active.verts[0].co
1245 axis = verts[1].co - verts[0].co
1247 # We will need a series of rotation matrices. We could use one which would be
1248 # faster but also might cause propagation of error.
1250 ## for i in range(numV):
1251 ## matrices.append(Matrix.Rotation((rads * i) + rotRange[0], 3, axis))
1252 matrices = [Matrix.Rotation((rads * i) + rotRange[0], 3, axis) for i in range(numV)]
1254 # New vertice coordinates:
1257 # If two edges were selected:
1258 # - If the lines are not parallel, then it will create a cone-like shaft
1259 if self.shaftType == 0:
1260 for i in range(len(verts) - 2):
1261 init_vec = distance_point_line(verts[i + 2].co, verts[0].co, verts[1].co)
1262 co = init_vec + verts[i + 2].co
1263 # These will be rotated about the orgin so will need to be shifted:
1264 for j in range(numV):
1265 verts_out.append(co - (matrices[j] * init_vec))
1266 elif self.shaftType == 1:
1268 init_vec = distance_point_line(i.co, active.verts[0].co, active.verts[1].co)
1269 co = init_vec + i.co
1270 # These will be rotated about the orgin so will need to be shifted:
1271 for j in range(numV):
1272 verts_out.append(co - (matrices[j] * init_vec))
1273 # Else if a line and a point was selected:
1274 elif self.shaftType == 2:
1275 init_vec = distance_point_line(verts[2].co, verts[0].co, verts[1].co)
1276 # These will be rotated about the orgin so will need to be shifted:
1277 verts_out = [(verts[i].co - (matrices[j] * init_vec)) for i in range(2) for j in range(numV)]
1278 # Else the above are not possible, so we will just use the edge:
1279 # - The vector defined by the edge is the normal of the plane for the shaft
1280 # - The shaft will have radius "radius".
1282 if is_axial(verts[0].co, verts[1].co) == None:
1283 proj = (verts[1].co - verts[0].co)
1285 norm = proj.cross(verts[1].co - verts[0].co)
1286 vec = norm.cross(verts[1].co - verts[0].co)
1287 vec.length = self.radius
1288 elif is_axial(verts[0].co, verts[1].co) == 'Z':
1289 vec = verts[0].co + Vector((0, 0, self.radius))
1291 vec = verts[0].co + Vector((0, self.radius, 0))
1292 init_vec = distance_point_line(vec, verts[0].co, verts[1].co)
1293 # These will be rotated about the orgin so will need to be shifted:
1294 verts_out = [(verts[i].co - (matrices[j] * init_vec)) for i in range(2) for j in range(numV)]
1296 # We should have the coordinates for a bunch of new verts. Now add the verts
1297 # and build the edges and then the faces.
1301 if self.shaftType == 1:
1303 for i in range(numV * len(verts)):
1305 new.co = verts_out[i]
1307 newVerts.append(new)
1310 for i in range(numE):
1311 for j in range(len(verts)):
1312 e = bEdges.new((newVerts[i + (numV * j)], newVerts[i + (numV * j) + 1]))
1314 for i in range(numV):
1315 for j in range(len(verts) - 1):
1316 e = bEdges.new((newVerts[i + (numV * j)], newVerts[i + (numV * (j + 1))]))
1320 # There is a problem with this right now:
1321 for i in range(len(edges)):
1322 for j in range(numE):
1323 f = bFaces.new((newVerts[i], newVerts[i + 1],
1324 newVerts[i + (numV * j) + 1], newVerts[i + (numV * j)]))
1328 for i in range(numV * 2):
1330 new.co = verts_out[i]
1332 newVerts.append(new)
1335 for i in range(numE):
1336 e = bEdges.new((newVerts[i], newVerts[i + 1]))
1338 e = bEdges.new((newVerts[i + numV], newVerts[i + numV + 1]))
1340 for i in range(numV):
1341 e = bEdges.new((newVerts[i], newVerts[i + numV]))
1345 for i in range(numE):
1346 f = bFaces.new((newVerts[i], newVerts[i + 1],
1347 newVerts[i + numV + 1], newVerts[i + numV]))
1350 bm.to_mesh(bpy.context.active_object.data)
1351 bpy.ops.object.editmode_toggle()
1355 # "Slices" edges crossing a plane defined by a face.
1356 class Slice(bpy.types.Operator):
1357 bl_idname = "mesh.edgetools_slice"
1359 bl_description = "Cuts edges at the plane defined by a selected face."
1360 bl_options = {'REGISTER', 'UNDO'}
1362 make_copy = BoolProperty(name = "Make Copy",
1363 description = "Make new vertices at intersection points instead of spliting the edge",
1365 rip = BoolProperty(name = "Rip",
1366 description = "Split into two edges that DO NOT share an intersection vertice.",
1368 pos = BoolProperty(name = "Positive",
1369 description = "Remove the portion on the side of the face normal",
1371 neg = BoolProperty(name = "Negative",
1372 description = "Remove the portion on the side opposite of the face normal",
1375 def draw(self, context):
1376 layout = self.layout
1378 layout.prop(self, "make_copy")
1379 if not self.make_copy:
1380 layout.prop(self, "rip")
1381 layout.label("Remove Side:")
1382 layout.prop(self, "pos")
1383 layout.prop(self, "neg")
1387 def poll(cls, context):
1388 ob = context.active_object
1389 return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
1392 def invoke(self, context, event):
1393 return self.execute(context)
1396 def execute(self, context):
1397 bpy.ops.object.editmode_toggle()
1399 bm.from_mesh(context.active_object.data)
1402 # For easy access to verts, edges, and faces:
1410 # Find the selected face. This will provide the plane to project onto:
1411 if isinstance(bm.select_history.active, bmesh.types.BMFace):
1412 face = bm.select_history.active
1413 normal = bm.select_history.active.normal
1414 bm.select_history.active.select = False
1424 bpy.ops.object.editmode_toggle()
1425 self.report({'ERROR_INVALID_INPUT'},
1426 "You must select a face as the cutting plane.")
1427 return {'CANCELLED'}
1428 elif len(face.verts) > 4 and not is_face_planar(face):
1429 self.report({'WARNING'},
1430 "Selected face is an n-gon. Results may be unpredictable.")
1435 if e.select and (v1 not in face.verts and v2 not in face.verts):
1436 if len(face.verts) < 5: # Not an n-gon
1437 intersection = intersect_line_face(e, face, True)
1439 intersection = intersect_line_plane(v1.co, v2.co, face.verts[0].co, normal)
1441 if intersection != None:
1442 d1 = distance_point_to_plane(v1.co, face.verts[0].co, normal)
1443 d2 = distance_point_to_plane(v2.co, face.verts[0].co, normal)
1444 # If they have different signs, then the edge crosses the plane:
1445 if abs(d1 + d2) < abs(d1 - d2):
1446 # Make the first vertice the positive vertice:
1451 new.co = intersection
1453 newV1 = bVerts.new()
1454 newV1.co = intersection
1455 newV2 = bVerts.new()
1456 newV2.co = intersection
1458 print("New vertices were successfully created")
1459 newE1 = bEdges.new((v1, newV1))
1460 newE2 = bEdges.new((v2, newV2))
1462 print("New edges were successfully created")
1465 print("Old edge successfully removed")
1467 new = list(bmesh.utils.edge_split(e, v1, 0.5))
1468 new[1].co = intersection
1470 new[0].select = False
1472 bEdges.remove(new[0])
1476 bm.to_mesh(context.active_object.data)
1477 bpy.ops.object.editmode_toggle()
1481 class Project(bpy.types.Operator):
1482 bl_idname = "mesh.edgetools_project"
1483 bl_label = "Project"
1484 bl_description = "Projects the selected vertices/edges onto the selected plane."
1485 bl_options = {'REGISTER', 'UNDO'}
1487 make_copy = BoolProperty(name = "Make Copy",
1488 description = "Make a duplicate of the vertices instead of moving it",
1491 def draw(self, context):
1492 layout = self.layout
1493 layout.prop(self, "make_copy")
1496 def poll(cls, context):
1497 ob = context.active_object
1498 return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
1501 def invoke(self, context, event):
1502 return self.execute(context)
1505 def execute(self, context):
1506 bpy.ops.object.editmode_toggle()
1508 bm.from_mesh(context.active_object.data)
1517 # Find the selected face. This will provide the plane to project onto:
1531 d = distance_point_to_plane(v.co, fVerts[0].co, normal)
1537 vector.length = abs(d)
1538 v.co = v.co - (vector * sign(d))
1541 bm.to_mesh(context.active_object.data)
1542 bpy.ops.object.editmode_toggle()
1546 # Project_End is for projecting/extending an edge to meet a plane.
1547 # This is used be selecting a face to define the plane then all the edges.
1548 # The add-on will then move the vertices in the edge that is closest to the
1549 # plane to the coordinates of the intersection of the edge and the plane.
1550 class Project_End(bpy.types.Operator):
1551 bl_idname = "mesh.edgetools_project_end"
1552 bl_label = "Project (End Point)"
1553 bl_description = "Projects the vertice of the selected edges closest to a plane onto that plane."
1554 bl_options = {'REGISTER', 'UNDO'}
1556 make_copy = BoolProperty(name = "Make Copy",
1557 description = "Make a duplicate of the vertice instead of moving it",
1559 keep_length = BoolProperty(name = "Keep Edge Length",
1560 description = "Maintain edge lengths",
1562 use_force = BoolProperty(name = "Use opposite vertices",
1563 description = "Force the usage of the vertices at the other end of the edge",
1565 use_normal = BoolProperty(name = "Project along normal",
1566 description = "Use the plane's normal as the projection direction",
1569 def draw(self, context):
1570 layout = self.layout
1571 ## layout.prop(self, "keep_length")
1572 if not self.keep_length:
1573 layout.prop(self, "use_normal")
1575 ## self.report({'ERROR_INVALID_INPUT'}, "Maintaining edge length not yet supported")
1576 ## self.report({'WARNING'}, "Projection may result in unexpected geometry")
1577 layout.prop(self, "make_copy")
1578 layout.prop(self, "use_force")
1582 def poll(cls, context):
1583 ob = context.active_object
1584 return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
1587 def invoke(self, context, event):
1588 return self.execute(context)
1591 def execute(self, context):
1592 bpy.ops.object.editmode_toggle()
1594 bm.from_mesh(context.active_object.data)
1603 # Find the selected face. This will provide the plane to project onto:
1616 if v1 in fVerts or v2 in fVerts:
1619 intersection = intersect_line_plane(v1.co, v2.co, fVerts[0].co, normal)
1620 if intersection != None:
1621 # Use abs because we don't care what side of plane we're on:
1622 d1 = distance_point_to_plane(v1.co, fVerts[0].co, normal)
1623 d2 = distance_point_to_plane(v2.co, fVerts[0].co, normal)
1624 # If d1 is closer than we use v1 as our vertice:
1625 # "xor" with 'use_force':
1626 if (abs(d1) < abs(d2)) is not self.use_force:
1629 v1.co = e.verts[0].co
1630 if self.keep_length:
1631 v1.co = intersection
1632 elif self.use_normal:
1634 vector.length = abs(d1)
1635 v1.co = v1.co - (vector * sign(d1))
1637 v1.co = intersection
1641 v2.co = e.verts[1].co
1642 if self.keep_length:
1643 v2.co = intersection
1644 elif self.use_normal:
1646 vector.length = abs(d2)
1647 v2.co = v2.co - (vector * sign(d2))
1649 v2.co = intersection
1652 bm.to_mesh(context.active_object.data)
1653 bpy.ops.object.editmode_toggle()
1659 # Blender currently does not have a CAD-style edge-based fillet function. This
1660 # is my atempt to create one. It should take advantage of BMesh and the ngon
1661 # capabilities for non-destructive modeling, if possible. This very well may
1662 # not result in nice quads and it will be up to the artist to clean up the mesh
1663 # back into quads if necessary.
1666 # - Faces are planar. This should, however, do a check an warn otherwise.
1668 # Developement Process:
1669 # Because this will eventaully prove to be a great big jumble of code and
1670 # various functionality, this is to provide an outline for the developement
1671 # and functionality wanted at each milestone.
1672 # 1) intersect_line_face: function to find the intersection point, if it
1673 # exists, at which a line intersects a face. The face does not have to
1674 # be planar, and can be an ngon. This will allow for a point to be placed
1675 # on the actual mesh-face for non-planar faces.
1676 # 2) Minimal propagation, single edge: Filleting of a single edge without
1677 # propagation of the fillet along "tangent" edges.
1678 # 3) Minimal propagation, multiple edges: Perform said fillet along/on
1680 # 4) "Tangency" detection code: because we have a mesh based geometry, this
1681 # have to make an educated guess at what is actually supposed to be
1682 # treated as tangent and what constitutes a sharp edge. This should
1683 # respect edges marked as sharp (does not propagate passed an
1684 # intersecting edge that is marked as sharp).
1685 # 5) Tangent propagation, single edge: Filleting of a single edge using the
1686 # above tangency detection code to continue the fillet to adjacent
1688 # 6) Tangent propagation, multiple edges: Same as above, but with multiple
1689 # edges selected. If multiple edges were selected along the same
1690 # tangency path, only one edge will be filleted. The others must be
1691 # ignored/discarded.
1692 class Fillet(bpy.types.Operator):
1693 bl_idname = "mesh.edgetools_fillet"
1694 bl_label = "Edge Fillet"
1695 bl_description = "Fillet the selected edges."
1696 bl_options = {'REGISTER', 'UNDO'}
1698 radius = FloatProperty(name = "Radius",
1699 description = "Radius of the edge fillet",
1700 min = 0.00001, max = 1024.0,
1702 prop = EnumProperty(name = "Propagation",
1703 items = [("m", "Minimal", "Minimal edge propagation"),
1704 ("t", "Tangential", "Tangential edge propagation")],
1706 prop_fac = FloatProperty(name = "Propagation Factor",
1707 description = "Corner detection sensitivity factor for tangential propagation",
1708 min = 0.0, max = 100.0,
1710 deg_seg = FloatProperty(name = "Degrees/Section",
1711 description = "Approximate degrees per section",
1712 min = 0.00001, max = 180.0,
1714 res = IntProperty(name = "Resolution",
1715 description = "Resolution of the fillet",
1716 min = 1, max = 1024,
1719 def draw(self, context):
1720 layout = self.layout
1721 layout.prop(self, "radius")
1722 layout.prop(self, "prop")
1723 if self.prop == "t":
1724 layout.prop(self, "prop_fac")
1725 layout.prop(self, "deg_seg")
1726 layout.prop(self, "res")
1730 def poll(cls, context):
1731 ob = context.active_object
1732 return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
1735 def invoke(self, context, event):
1736 return self.execute(context)
1739 def execute(self, context):
1740 bpy.ops.object.editmode_toggle()
1742 bm.from_mesh(bpy.context.active_object.data)
1749 # Robustness check: this does not support n-gons (at least for now)
1750 # because I have no idea how to handle them righ now. If there is
1751 # an n-gon in the mesh, warn the user that results may be nuts because
1754 # I'm not going to cause it to exit if there are n-gons, as they may
1755 # not be encountered.
1756 # @todo I would like this to be a confirmation dialoge of some sort
1757 # @todo I would REALLY like this to just handle n-gons. . . .
1759 if len(face.verts) > 4:
1760 self.report({'WARNING'},
1761 "Mesh contains n-gons which are not supported. Operation may fail.")
1764 # Get the selected edges:
1765 # Robustness check: boundary and wire edges are not fillet-able.
1766 edges = [e for e in bEdges if e.select and not e.is_boundary and not e.is_wire]
1769 axis_points = fillet_axis(e, self.radius)
1772 bm.to_mesh(bpy.context.active_object.data)
1773 bpy.ops.object.editmode_toggle()
1777 # For testing the mess that is "intersect_line_face" for possible math errors.
1778 # This will NOT be directly exposed to end users: it will always require running
1779 # Blender in debug mode.
1780 # So far no errors have been found. Thanks to anyone who tests and reports bugs!
1781 class Intersect_Line_Face(bpy.types.Operator):
1782 bl_idname = "mesh.edgetools_ilf"
1783 bl_label = "ILF TEST"
1784 bl_description = "TEST ONLY: INTERSECT_LINE_FACE"
1785 bl_options = {'REGISTER', 'UNDO'}
1788 def poll(cls, context):
1789 ob = context.active_object
1790 return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
1793 def invoke(self, context, event):
1794 return self.execute(context)
1797 def execute(self, context):
1798 # Make sure we really are in debug mode:
1799 if not bpy.app.debug:
1800 self.report({'ERROR_INVALID_INPUT'},
1801 "This is for debugging only: you should not be able to run this!")
1802 return {'CANCELLED'}
1804 bpy.ops.object.editmode_toggle()
1806 bm.from_mesh(bpy.context.active_object.data)
1821 if e.select and not e in face.edges:
1825 point = intersect_line_face(edge, face, True)
1831 bpy.ops.object.editmode_toggle()
1832 self.report({'ERROR_INVALID_INPUT'}, "point was \"None\"")
1833 return {'CANCELLED'}
1835 bm.to_mesh(bpy.context.active_object.data)
1836 bpy.ops.object.editmode_toggle()
1840 class VIEW3D_MT_edit_mesh_edgetools(bpy.types.Menu):
1841 bl_label = "EdgeTools"
1843 def draw(self, context):
1844 layout = self.layout
1846 layout.operator("mesh.edgetools_extend")
1847 layout.operator("mesh.edgetools_spline")
1848 layout.operator("mesh.edgetools_ortho")
1849 layout.operator("mesh.edgetools_shaft")
1850 layout.operator("mesh.edgetools_slice")
1851 layout.operator("mesh.edgetools_project")
1852 layout.operator("mesh.edgetools_project_end")
1854 ## Not ready for prime-time yet:
1855 layout.operator("mesh.edgetools_fillet")
1856 ## For internal testing ONLY:
1857 layout.operator("mesh.edgetools_ilf")
1860 def menu_func(self, context):
1861 self.layout.menu("VIEW3D_MT_edit_mesh_edgetools")
1862 self.layout.separator()
1865 # define classes for registration
1866 classes = [VIEW3D_MT_edit_mesh_edgetools,
1875 Intersect_Line_Face]
1878 # registering and menu integration
1880 if int(bpy.app.build_revision[0:5]) < 44800:
1881 print("Error in Edgetools:")
1882 print("This version of Blender does not support the necessary BMesh API.")
1883 print("Please download Blender 2.63 or newer.")
1886 bpy.utils.register_class(c)
1887 bpy.types.VIEW3D_MT_edit_mesh_specials.prepend(menu_func)
1890 # unregistering and removing menus
1893 bpy.utils.unregister_class(c)
1894 bpy.types.VIEW3D_MT_edit_mesh_specials.remove(menu_func)
1897 if __name__ == "__main__":