Updates from the past couple months. The primary changes have been:
[blender-addons-contrib.git] / mesh_edgetools.py
1 # Blender EdgeTools
2 #
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.
9 #
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?
13 #
14 # The GUI and Blender add-on structure shamelessly coded in imitation of the
15 # LoopTools addon.
16 #
17 # Examples:
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.
28 #
29 # Tasks:
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.
33 #
34 # Paul "BrikBot" Marshall
35 # Created: January 28, 2012
36 # Last Modified: June 10, 2012
37 # Homepage (blog): http://post.darkarsenic.com/
38 #                       //blog.darkarsenic.com/
39 #
40 # Coded in IDLE, tested in Blender 2.63.
41 # Search for "@todo" to quickly find sections that need work.
42 #
43 # Remeber -
44 #   Functional code comes before fast code.  Once it works, then worry about
45 #   making it faster/more efficient.
46 #
47 # ##### BEGIN GPL LICENSE BLOCK #####
48 #
49 #  The Blender Edgetools is to bring CAD tools to Blender.
50 #  Copyright (C) 2012  Paul Marshall
51 #
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.
56 #
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.
61 #
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/>.
64 #
65 # ##### END GPL LICENSE BLOCK #####
66
67 # <pep8 compliant>
68 # ^^ Maybe. . . . :P
69
70 bl_info = {
71     'name': "EdgeTools",
72     'author': "Paul Marshall",
73     'version': (0, 8),
74     'blender': (2, 6, 3),
75     'location': "View3D > Toolbar and View3D > Specials (W-key)",
76     'warning': "",
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",
80     'category': 'Mesh'}
81
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,
86                                 interpolate_bezier,
87                                 intersect_point_line,
88                                 intersect_line_line,
89                                 intersect_line_plane)
90 from bpy.props import (BoolProperty,
91                        BoolVectorProperty,
92                        IntProperty,
93                        FloatProperty,
94                        EnumProperty)
95
96 # Quick an dirty method for getting the sign of a number:
97 def sign(number):
98     return (number > 0) - (number < 0)
99
100
101 # is_parallel
102 #
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
107
108
109 # is_axial
110 #
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):
115     vector = v2 - v1
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:
121         return 'Z'
122     elif vec0 and vec1:
123         return 'Y'
124     return None
125
126
127 # is_same_co
128 #
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):
133         return False
134     else:
135         for co1, co2 in zip(v1, v2):
136             if co1 != co2:
137                 return False
138     return True
139
140
141 # is_face_planar
142 #
143 # Tests a face to see if it is planar.
144 def is_face_planar(face, error = 0.000002):
145     for v in face.verts:
146         d = distance_point_to_plane(v.co, face.verts[0].co, face.normal)
147         if d < -error or d > error:
148             return False
149     return True
150
151
152 # other_joined_edges
153 #
154 # Starts with an edge.  Then scans for linked, selected edges and builds a
155 # list with them in "order", starting at one end and moving towards the other.
156 def order_joined_edges(edge, edges = [], direction = 1):
157     if len(edges) == 0:
158         edges.append(edge)
159         edges[0] = edge
160
161     if bpy.app.debug:
162         print(edge, end = ", ")
163         print(edges, end = ", ")
164         print(direction, end = "; ")
165
166     # Robustness check: direction cannot be zero
167     if direction == 0:
168         direction = 1
169
170     newList = []
171     for e in edge.verts[0].link_edges:
172         if e.select and edges.count(e) == 0:
173             if direction > 0:
174                 edges.insert(0, e)
175                 newList.extend(order_joined_edges(e, edges, direction + 1))
176                 newList.extend(edges)
177             else:
178                 edges.append(e)
179                 newList.extend(edges)
180                 newList.extend(order_joined_edges(e, edges, direction - 1))
181
182     # This will only matter at the first level:
183     direction = direction * -1
184
185     for e in edge.verts[1].link_edges:
186         if e.select and edges.count(e) == 0:
187             if direction > 0:
188                 edges.insert(0, e)
189                 newList.extend(order_joined_edges(e, edges, direction + 2))
190                 newList.extend(edges)
191             else:
192                 edges.append(e)
193                 newList.extend(edges)
194                 newList.extend(order_joined_edges(e, edges, direction))
195
196     if bpy.app.debug:
197         print(newList, end = ", ")
198         print(direction)
199
200     return newList
201
202
203 # --------------- GEOMETRY CALCULATION METHODS --------------
204
205 # distance_point_line
206 #
207 # I don't know why the mathutils.geometry API does not already have this, but
208 # it is trivial to code using the structures already in place.  Instead of
209 # returning a float, I also want to know the direction vector defining the
210 # distance.  Distance can be found with "Vector.length".
211 def distance_point_line(pt, line_p1, line_p2):
212     int_co = intersect_point_line(pt, line_p1, line_p2)
213     distance_vector = int_co[0] - pt
214     return distance_vector
215
216
217 # interpolate_line_line
218 #
219 # This is an experiment into a cubic Hermite spline (c-spline) for connecting
220 # two edges with edges that obey the general equation.
221 # This will return a set of point coordinates (Vectors).
222 #
223 # A good, easy to read background on the mathematics can be found at:
224 # http://cubic.org/docs/hermite.htm
225 #
226 # Right now this is . . . less than functional :P
227 # @todo
228 #   - C-Spline and Bezier curves do not end on p2_co as they are supposed to.
229 #   - B-Spline just fails.  Epically.
230 #   - Add more methods as I come across them.  Who said flexibility was bad?
231 def interpolate_line_line(p1_co, p1_dir, p2_co, p2_dir, segments, tension = 1,
232                           typ = 'BEZIER', include_ends = False):
233     pieces = []
234     fraction = 1 / segments
235     # Form: p1, tangent 1, p2, tangent 2
236     if typ == 'HERMITE':
237         poly = [[2, -3, 0, 1], [1, -2, 1, 0],
238                 [-2, 3, 0, 0], [1, -1, 0, 0]]
239     elif typ == 'BEZIER':
240         poly = [[-1, 3, -3, 1], [3, -6, 3, 0],
241                 [1, 0, 0, 0], [-3, 3, 0, 0]]
242         p1_dir = p1_dir + p1_co
243         p2_dir = -p2_dir + p2_co
244     elif typ == 'BSPLINE':
245 ##        Supposed poly matrix for a cubic b-spline:
246 ##        poly = [[-1, 3, -3, 1], [3, -6, 3, 0],
247 ##                [-3, 0, 3, 0], [1, 4, 1, 0]]
248         # My own invention to try to get something that somewhat acts right.
249         # This is semi-quadratic rather than fully cubic:
250         poly = [[0, -1, 0, 1], [1, -2, 1, 0],
251                 [0, -1, 2, 0], [1, -1, 0, 0]]
252     if include_ends:
253         pieces.append(p1_co)
254     # Generate each point:
255     for i in range(segments - 1):
256         t = fraction * (i + 1)
257         if bpy.app.debug:
258             print(t)
259         s = [t ** 3, t ** 2, t, 1]
260         h00 = (poly[0][0] * s[0]) + (poly[0][1] * s[1]) + (poly[0][2] * s[2]) + (poly[0][3] * s[3])
261         h01 = (poly[1][0] * s[0]) + (poly[1][1] * s[1]) + (poly[1][2] * s[2]) + (poly[1][3] * s[3])
262         h10 = (poly[2][0] * s[0]) + (poly[2][1] * s[1]) + (poly[2][2] * s[2]) + (poly[2][3] * s[3])
263         h11 = (poly[3][0] * s[0]) + (poly[3][1] * s[1]) + (poly[3][2] * s[2]) + (poly[3][3] * s[3])
264         pieces.append((h00 * p1_co) + (h01 * p1_dir) + (h10 * p2_co) + (h11 * p2_dir))
265     if include_ends:
266         pieces.append(p2_co)
267     # Return:
268     if len(pieces) == 0:
269         return None
270     else:
271         if bpy.app.debug:
272             print(pieces)
273         return pieces
274
275
276 # intersect_line_face
277 #
278 # Calculates the coordinate of intersection of a line with a face.  It returns
279 # the coordinate if one exists, otherwise None.  It can only deal with tris or
280 # quads for a face.  A quad does NOT have to be planar. Thus the following.
281 #
282 # Quad math and theory:
283 # A quad may not be planar.  Therefore the treated definition of the surface is
284 # that the surface is composed of all lines bridging two other lines defined by
285 # the given four points.  The lines do not "cross".
286
287 # The two lines in 3-space can defined as:
288 #   ┌  ┐         ┌   ┐     ┌   ┐  ┌  ┐         ┌   ┐     ┌   ┐
289 #   │x1│         │a11│     │b11│  │x2│         │a21│     │b21│
290 #   │y1│ = (1-t1)│a12│ + t1│b12│, │y2│ = (1-t2)│a22│ + t2│b22│
291 #   │z1│         │a13│     │b13│  │z2│         │a23│     │b23│
292 #   └  ┘         └   ┘     └   ┘  └  ┘         └   ┘     └   ┘
293 # Therefore, the surface is the lines defined by every point alone the two
294 # lines with a same "t" value (t1 = t2).  This is basically R = V1 + tQ, where
295 # Q = V2 - V1 therefore R = V1 + t(V2 - V1) -> R = (1 - t)V1 + tV2:
296 #   ┌   ┐            ┌                  ┐      ┌                  ┐
297 #   │x12│            │(1-t)a11 + t * b11│      │(1-t)a21 + t * b21│
298 #   │y12│ = (1 - t12)│(1-t)a12 + t * b12│ + t12│(1-t)a22 + t * b22│
299 #   │z12│            │(1-t)a13 + t * b13│      │(1-t)a23 + t * b23│
300 #   └   ┘            └                  ┘      └                  ┘
301 # Now, the equation of our line can be likewise defined:
302 #   ┌  ┐   ┌   ┐     ┌   ┐
303 #   │x3│   │a31│     │b31│
304 #   │y3│ = │a32│ + t3│b32│
305 #   │z3│   │a33│     │b33│
306 #   └  ┘   └   ┘     └   ┘
307 # Now we just have to find a valid solution for the two equations.  This should
308 # be our point of intersection.  Therefore, x12 = x3 -> x, y12 = y3 -> y,
309 # z12 = z3 -> z.  Thus, to find that point we set the equation defining the
310 # surface as equal to the equation for the line:
311 #            ┌                  ┐      ┌                  ┐   ┌   ┐     ┌   ┐
312 #            │(1-t)a11 + t * b11│      │(1-t)a21 + t * b21│   │a31│     │b31│
313 #   (1 - t12)│(1-t)a12 + t * b12│ + t12│(1-t)a22 + t * b22│ = │a32│ + t3│b32│
314 #            │(1-t)a13 + t * b13│      │(1-t)a23 + t * b23│   │a33│     │b33│
315 #            └                  ┘      └                  ┘   └   ┘     └   ┘
316 # This leaves us with three equations, three unknowns.  Solving the system by
317 # hand is practically impossible, but using Mathematica we are given an insane
318 # series of three equations (not reproduced here for the sake of space: see
319 # http://www.mediafire.com/file/cc6m6ba3sz2b96m/intersect_line_surface.nb and
320 # http://www.mediafire.com/file/0egbr5ahg14talm/intersect_line_surface2.nb for
321 # Mathematica computation).
322 #
323 # Additionally, the resulting series of equations may result in a div by zero
324 # exception if the line in question if parallel to one of the axis or if the
325 # quad is planar and parallel to either the XY, XZ, or YZ planes.  However, the
326 # system is still solvable but must be dealt with a little differently to avaid
327 # these special cases.  Because the resulting equations are a little different,
328 # we have to code them differently.  Hence the special cases.
329 #
330 # Tri math and theory:
331 # A triangle must be planar (three points define a plane).  Therefore we just
332 # have to make sure that the line intersects inside the triangle.
333 #
334 # If the point is within the triangle, then the angle between the lines that
335 # connect the point to the each individual point of the triangle will be
336 # equal to 2 * PI.  Otherwise, if the point is outside the triangle, then the
337 # sum of the angles will be less.
338 #
339 # @todo
340 #   - Figure out how to deal with n-gons.  How the heck is a face with 8 verts
341 #       definied mathematically?  How do I then find the intersection point of
342 #       a line with said vert?  How do I know if that point is "inside" all the
343 #       verts?  I have no clue, and haven't been able to find anything on it so
344 #       far.  Maybe if someone (actually reads this and) who knows could note?
345 def intersect_line_face(edge, face, is_infinite = False, error = 0.000002):
346     int_co = None
347
348     # If we are dealing with a non-planar quad:
349     if len(face.verts) == 4 and not is_face_planar(face):
350         edgeA = face.edges[0]
351         edgeB = None
352         flipB = False
353
354         for i in range(len(face.edges)):
355             if face.edges[i].verts[0] not in edgeA.verts and face.edges[i].verts[1] not in edgeA.verts:
356                 edgeB = face.edges[i]
357                 break
358
359         # I haven't figured out a way to mix this in with the above.  Doing so might remove a
360         # few extra instructions from having to be executed saving a few clock cycles:
361         for i in range(len(face.edges)):
362             if face.edges[i] == edgeA or face.edges[i] == edgeB:
363                 continue
364             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):
365                 flipB = True
366                 break
367
368         # Define calculation coefficient constants:
369         # "xx1" is the x coordinate, "xx2" is the y coordinate, and "xx3" is the z
370         # coordinate.
371         a11, a12, a13 = edgeA.verts[0].co[0], edgeA.verts[0].co[1], edgeA.verts[0].co[2]
372         b11, b12, b13 = edgeA.verts[1].co[0], edgeA.verts[1].co[1], edgeA.verts[1].co[2]
373         if flipB:
374             a21, a22, a23 = edgeB.verts[1].co[0], edgeB.verts[1].co[1], edgeB.verts[1].co[2]
375             b21, b22, b23 = edgeB.verts[0].co[0], edgeB.verts[0].co[1], edgeB.verts[0].co[2]
376         else:
377             a21, a22, a23 = edgeB.verts[0].co[0], edgeB.verts[0].co[1], edgeB.verts[0].co[2]
378             b21, b22, b23 = edgeB.verts[1].co[0], edgeB.verts[1].co[1], edgeB.verts[1].co[2]
379         a31, a32, a33 = edge.verts[0].co[0], edge.verts[0].co[1], edge.verts[0].co[2]
380         b31, b32, b33 = edge.verts[1].co[0], edge.verts[1].co[1], edge.verts[1].co[2]
381
382         # There are a bunch of duplicate "sub-calculations" inside the resulting
383         # equations for t, t12, and t3.  Calculate them once and store them to
384         # reduce computational time:
385         m01 = a13 * a22 * a31
386         m02 = a12 * a23 * a31
387         m03 = a13 * a21 * a32
388         m04 = a11 * a23 * a32
389         m05 = a12 * a21 * a33
390         m06 = a11 * a22 * a33
391         m07 = a23 * a32 * b11
392         m08 = a22 * a33 * b11
393         m09 = a23 * a31 * b12
394         m10 = a21 * a33 * b12
395         m11 = a22 * a31 * b13
396         m12 = a21 * a32 * b13
397         m13 = a13 * a32 * b21
398         m14 = a12 * a33 * b21
399         m15 = a13 * a31 * b22
400         m16 = a11 * a33 * b22
401         m17 = a12 * a31 * b23
402         m18 = a11 * a32 * b23
403         m19 = a13 * a22 * b31
404         m20 = a12 * a23 * b31
405         m21 = a13 * a32 * b31
406         m22 = a23 * a32 * b31
407         m23 = a12 * a33 * b31
408         m24 = a22 * a33 * b31
409         m25 = a23 * b12 * b31
410         m26 = a33 * b12 * b31
411         m27 = a22 * b13 * b31
412         m28 = a32 * b13 * b31
413         m29 = a13 * b22 * b31
414         m30 = a33 * b22 * b31
415         m31 = a12 * b23 * b31
416         m32 = a32 * b23 * b31
417         m33 = a13 * a21 * b32
418         m34 = a11 * a23 * b32
419         m35 = a13 * a31 * b32
420         m36 = a23 * a31 * b32
421         m37 = a11 * a33 * b32
422         m38 = a21 * a33 * b32
423         m39 = a23 * b11 * b32
424         m40 = a33 * b11 * b32
425         m41 = a21 * b13 * b32
426         m42 = a31 * b13 * b32
427         m43 = a13 * b21 * b32
428         m44 = a33 * b21 * b32
429         m45 = a11 * b23 * b32
430         m46 = a31 * b23 * b32
431         m47 = a12 * a21 * b33
432         m48 = a11 * a22 * b33
433         m49 = a12 * a31 * b33
434         m50 = a22 * a31 * b33
435         m51 = a11 * a32 * b33
436         m52 = a21 * a32 * b33
437         m53 = a22 * b11 * b33
438         m54 = a32 * b11 * b33
439         m55 = a21 * b12 * b33
440         m56 = a31 * b12 * b33
441         m57 = a12 * b21 * b33
442         m58 = a32 * b21 * b33
443         m59 = a11 * b22 * b33
444         m60 = a31 * b22 * b33
445         m61 = a33 * b12 * b21
446         m62 = a32 * b13 * b21
447         m63 = a33 * b11 * b22
448         m64 = a31 * b13 * b22
449         m65 = a32 * b11 * b23
450         m66 = a31 * b12 * b23
451         m67 = b13 * b22 * b31
452         m68 = b12 * b23 * b31
453         m69 = b13 * b21 * b32
454         m70 = b11 * b23 * b32
455         m71 = b12 * b21 * b33
456         m72 = b11 * b22 * b33
457         n01 = m01 - m02 - m03 + m04 + m05 - m06
458         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
459         n03 = -m19 + m20 + m33 - m34 - m47 + m48
460         n04 = m21 - m22 - m23 + m24 - m35 + m36 + m37 - m38 + m49 - m50 - m51 + m52
461         n05 = m26 - m28 - m30 + m32 - m40 + m42 + m44 - m46 + m54 - m56 - m58 + m60
462         n06 = m61 - m62 - m63 + m64 + m65 - m66 - m67 + m68 + m69 - m70 - m71 + m72
463         n07 = 2 * n01 + n02 + 2 * n03 + n04 + n05
464         n08 = n01 + n02 + n03 + n06
465
466         # Calculate t, t12, and t3:
467         t = (n07 - sqrt(pow(-n07, 2) - 4 * (n01 + n03 + n04) * n08)) / (2 * n08)
468
469         # t12 can be greatly simplified by defining it with t in it:
470         # If block used to help prevent any div by zero error.
471         t12 = 0
472
473         if a31 == b31:
474             # The line is parallel to the z-axis:
475             if a32 == b32:
476                 t12 = ((a11 - a31) + (b11 - a11) * t) / ((a21 - a11) + (a11 - a21 - b11 + b21) * t)
477             # The line is parallel to the y-axis:
478             elif a33 == b33:
479                 t12 = ((a11 - a31) + (b11 - a11) * t) / ((a21 - a11) + (a11 - a21 - b11 + b21) * t)
480             # The line is along the y/z-axis but is not parallel to either:
481             else:
482                 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))
483         elif a32 == b32:
484             # The line is parallel to the x-axis:
485             if a33 == b33:
486                 t12 = ((a12 - a32) + (b12 - a12) * t) / ((a22 - a12) + (a12 - a22 - b12 + b22) * t)
487             # The line is along the x/z-axis but is not parallel to either:
488             else:
489                 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))
490         # The line is along the x/y-axis but is not parallel to either:
491         else:
492             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))
493
494         # Likewise, t3 is greatly simplified by defining it in terms of t and t12:
495         # If block used to prevent a div by zero error.
496         t3 = 0
497         if a31 != b31:
498             t3 = (-a11 + a31 + (a11 - b11) * t + (a11 - a21) * t12 + (a21 - a11 + b11 - b21) * t * t12) / (a31 - b31)
499         elif a32 != b32:
500             t3 = (-a12 + a32 + (a12 - b12) * t + (a12 - a22) * t12 + (a22 - a12 + b12 - b22) * t * t12) / (a32 - b32)
501         elif a33 != b33:
502             t3 = (-a13 + a33 + (a13 - b13) * t + (a13 - a23) * t12 + (a23 - a13 + b13 - b23) * t * t12) / (a33 - b33)
503         else:
504             print("The second edge is a zero-length edge")
505             return None
506
507         # Calculate the point of intersection:
508         x = (1 - t3) * a31 + t3 * b31
509         y = (1 - t3) * a32 + t3 * b32
510         z = (1 - t3) * a33 + t3 * b33
511         int_co = Vector((x, y, z))
512         
513         if bpy.app.debug:
514             print(int_co)
515
516         # If the line does not intersect the quad, we return "None":
517         if (t < -1 or t > 1 or t12 < -1 or t12 > 1) and not is_infinite:
518             int_co = None
519
520     elif len(face.verts) == 3:
521         p1, p2, p3 = face.verts[0].co, face.verts[1].co, face.verts[2].co
522         int_co = intersect_line_plane(edge.verts[0].co, edge.verts[1].co, p1, face.normal)
523
524         if int_co != None:
525             pA = p1 - int_co
526             pB = p2 - int_co
527             pC = p3 - int_co
528             aAB = acos(pA.dot(pB))
529             aBC = acos(pB.dot(pC))
530             aCA = acos(pC.dot(pA))
531             sumA = aAB + aBC + aCA
532
533             # If the point is outside the triangle:
534             if (sumA > (pi + error) and sumA < (pi - error)) and not is_infinite:
535                 int_co = None
536
537     # This is the default case where we either have a planar quad or an n-gon.
538     else:
539         int_co = intersect_line_plane(edge.verts[0].co, edge.verts[1].co,
540                                       face.verts[0].co, face.normal)
541
542     return int_co
543
544
545 # project_point_plane
546 #
547 # Projects a point onto a plane.  Returns a tuple of the projection vector
548 # and the projected coordinate.
549 def project_point_plane(pt, plane_co, plane_no):
550     proj_co = intersect_line_plane(pt, pt + plane_no, plane_co, plane_no)
551     proj_ve = proj_co - pt
552     return (proj_ve, proj_co)
553     
554
555 # ------------ FILLET/CHAMPHER HELPER METHODS -------------
556
557 # get_next_edge
558 #
559 # The following is used to return edges that might be possible edges for
560 # propagation.  If an edge is connected to the end vert, but is also a part
561 # of the on of the faces that the current edge composes, then it is a
562 # "corner edge" and is not valid as a propagation edge.  If the edge is
563 # part of two faces that a in the same plane, then we cannot fillet/chamfer
564 # it because there is no angle between them.
565 def get_next_edge(edge, vert):
566     invalidEdges = [e for f in edge.link_faces for e in f.edges if e != edge]
567     invalidEdges.append(edge)
568     if bpy.app.debug:
569         print(invalidEdges)
570     newEdge = [e for e in vert.link_edges if e not in invalidEdges and not is_planar_edge(e)]
571     if len(newEdge) == 0:
572         return None
573     elif len(newEdge) == 1:
574         return newEdge[0]
575     else:
576         return newEdge
577
578
579 def is_planar_edge(edge, error = 0.000002):
580     angle = edge.calc_face_angle()
581     return (angle < error and angle > -error) or (angle < (180 + error) and angle > (180 - error))
582
583
584 # fillet_axis
585 #
586 # Calculates the base geometry data for the fillet. This assumes that the faces
587 # are planar:
588 #
589 # @todo
590 #   - Redesign so that the faces do not have to be planar
591 #
592 # There seems to be issues some of the vector math right now.  Will need to be
593 # debuged.
594 def fillet_axis(edge, radius):
595     vectors = [None, None, None, None]
596     
597     origin = Vector((0, 0, 0))
598     axis = edge.verts[1].co - edge.verts[0].co
599
600     # Get the "adjacency" base vectors for face 0:
601     for e in edge.link_faces[0].edges:
602         if e == edge:
603             continue
604         if e.verts[0] == edge.verts[0]:
605             vectors[0] = e.verts[1].co - e.verts[0].co
606         elif e.verts[1] == edge.verts[0]:
607             vectors[0] = e.verts[0].co - e.verts[1].co
608         elif e.verts[0] == edge.verts[1]:
609             vectors[1] = e.verts[1].co - e.verts[0].co
610         elif e.verts[1] == edge.verts[1]:
611             vectors[1] = e.verts[0].co - e.verts[1].co
612
613     # Get the "adjacency" base vectors for face 1:
614     for e in edge.link_faces[1].edges:
615         if e == edge:
616             continue
617         if e.verts[0] == edge.verts[0]:
618             vectors[2] = e.verts[1].co - e.verts[0].co
619         elif e.verts[1] == edge.verts[0]:
620             vectors[2] = e.verts[0].co - e.verts[1].co
621         elif e.verts[0] == edge.verts[1]:
622             vectors[3] = e.verts[1].co - e.verts[0].co
623         elif e.verts[1] == edge.verts[1]:
624             vectors[3] = e.verts[0].co - e.verts[1].co
625
626     # Get the normal for face 0 and face 1:
627     norm1 = edge.link_faces[0].normal
628     norm2 = edge.link_faces[1].normal
629     
630     # We need to find the angle between the two faces, then bisect it:
631     theda = (pi - edge.calc_face_angle()) / 2
632     
633     # We are dealing with a triangle here, and we will need the length
634     # of its adjacent side.  The opposite is the radius:
635     adj_len = radius / tan(theda)
636
637     # Vectors can be thought of as being at the origin, and we need to make sure
638     # that the base vectors are planar with the "normal" definied by the edge to
639     # be filleted.  Then we set the length of the vector and shift it into a
640     # coordinate:
641     for i in range(len(vectors)):
642         vectors[i] = project_point_plane(vectors[i], origin, axis)[1]
643         vectors[i].length = adj_len
644         vectors[i] = vectors[i] + edge.verts[i % 2].co
645     
646     # Compute fillet axis end points:
647     v1 = intersect_line_line(vectors[0], vectors[0] + norm1, vectors[2], vectors[2] + norm2)[0]
648     v2 = intersect_line_line(vectors[1], vectors[1] + norm1, vectors[3], vectors[3] + norm2)[0]
649     return [v1, v2]
650
651
652 def fillet_point(t, face1, face2):
653     return
654
655
656 # ------------------- EDGE TOOL METHODS -------------------
657
658 # Extends an "edge" in two directions:
659 #   - Requires two vertices to be selected.  They do not have to form an edge.
660 #   - Extends "length" in both directions
661 class Extend(bpy.types.Operator):
662     bl_idname = "mesh.edgetools_extend"
663     bl_label = "Extend"
664     bl_description = "Extend the selected edges of vertice pair."
665     bl_options = {'REGISTER', 'UNDO'}
666
667     di1 = BoolProperty(name = "Forwards",
668                        description = "Extend the edge forwards",
669                        default = True)
670     di2 = BoolProperty(name = "Backwards",
671                        description = "Extend the edge backwards",
672                        default = False)
673     length = FloatProperty(name = "Length",
674                            description = "Length to extend the edge",
675                            min = 0.0, max = 1024.0,
676                            default = 1.0)
677
678     def draw(self, context):
679         layout = self.layout
680         layout.prop(self, "di1")
681         layout.prop(self, "di2")
682         layout.prop(self, "length")
683     
684
685     @classmethod
686     def poll(cls, context):
687         ob = context.active_object
688         return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
689
690
691     def invoke(self, context, event):
692         return self.execute(context)
693
694     
695     def execute(self, context):
696         bpy.ops.object.editmode_toggle()
697         bm = bmesh.new()
698         bm.from_mesh(bpy.context.active_object.data)
699         bm.normal_update()
700
701         bEdges = bm.edges
702         bVerts = bm.verts
703
704         edges = [e for e in bEdges if e.select]
705         verts = [v for v in bVerts if v.select]
706
707         if len(edges) > 0:
708             for e in edges:
709                 vector = e.verts[0].co - e.verts[1].co
710                 vector.length = self.length
711                 
712                 if self.di1:
713                     v = bVerts.new()
714                     if (vector[0] + vector[1] + vector[2]) < 0:
715                         v.co = e.verts[1].co - vector
716                         newE = bEdges.new((e.verts[1], v))
717                     else:
718                         v.co = e.verts[0].co + vector
719                         newE = bEdges.new((e.verts[0], v))
720                 if self.di2:
721                     v = bVerts.new()
722                     if (vector[0] + vector[1] + vector[2]) < 0:
723                         v.co = e.verts[0].co + vector
724                         newE = bEdges.new((e.verts[0], v))
725                     else:
726                         v.co = e.verts[1].co - vector
727                         newE = bEdges.new((e.verts[1], v))
728         else:
729             vector = verts[0].co - verts[1].co
730             vector.length = self.length
731
732             if self.di1:
733                 v = bVerts.new()
734                 if (vector[0] + vector[1] + vector[2]) < 0:
735                     v.co = verts[1].co - vector
736                     e = bEdges.new((verts[1], v))
737                 else:
738                     v.co = verts[0].co + vector
739                     e = bEdges.new((verts[0], v))
740             if self.di2:
741                 v = bVerts.new()
742                 if (vector[0] + vector[1] + vector[2]) < 0:
743                     v.co = verts[0].co + vector
744                     e = bEdges.new((verts[0], v))
745                 else:
746                     v.co = verts[1].co - vector
747                     e = bEdges.new((verts[1], v))
748
749         bm.to_mesh(bpy.context.active_object.data)
750         bpy.ops.object.editmode_toggle()
751         return {'FINISHED'}
752
753
754 # Creates a series of edges between two edges using spline interpolation.
755 # This basically just exposes existing functionality in addition to some
756 # other common methods: Hermite (c-spline), Bezier, and b-spline.  These
757 # alternates I coded myself after some extensive research into spline
758 # theory.
759 #
760 # @todo Figure out what's wrong with the Blender bezier interpolation.
761 class Spline(bpy.types.Operator):
762     bl_idname = "mesh.edgetools_spline"
763     bl_label = "Spline"
764     bl_description = "Create a spline interplopation between two edges"
765     bl_options = {'REGISTER', 'UNDO'}
766     
767     alg = EnumProperty(name = "Spline Algorithm",
768                        items = [('Blender', 'Blender', 'Interpolation provided through \"mathutils.geometry\"'),
769                                 ('Hermite', 'C-Spline', 'C-spline interpolation'),
770                                 ('Bezier', 'Bézier', 'Bézier interpolation'),
771                                 ('B-Spline', 'B-Spline', 'B-Spline interpolation')],
772                        default = 'Bezier')
773     segments = IntProperty(name = "Segments",
774                            description = "Number of segments to use in the interpolation",
775                            min = 2, max = 4096,
776                            soft_max = 1024,
777                            default = 32)
778     flip1 = BoolProperty(name = "Flip Edge",
779                          description = "Flip the direction of the spline on edge 1",
780                          default = False)
781     flip2 = BoolProperty(name = "Flip Edge",
782                          description = "Flip the direction of the spline on edge 2",
783                          default = False)
784     ten1 = FloatProperty(name = "Tension",
785                          description = "Tension on edge 1",
786                          min = -4096.0, max = 4096.0,
787                          soft_min = -8.0, soft_max = 8.0,
788                          default = 1.0)
789     ten2 = FloatProperty(name = "Tension",
790                          description = "Tension on edge 2",
791                          min = -4096.0, max = 4096.0,
792                          soft_min = -8.0, soft_max = 8.0,
793                          default = 1.0)
794
795     def draw(self, context):
796         layout = self.layout
797
798         layout.prop(self, "alg")
799         layout.prop(self, "segments")
800         layout.label("Edge 1:")
801         layout.prop(self, "ten1")
802         layout.prop(self, "flip1")
803         layout.label("Edge 2:")
804         layout.prop(self, "ten2")
805         layout.prop(self, "flip2")
806
807
808     @classmethod
809     def poll(cls, context):
810         ob = context.active_object
811         return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
812
813
814     def invoke(self, context, event):
815         return self.execute(context)
816
817     
818     def execute(self, context):
819         bpy.ops.object.editmode_toggle()
820         bm = bmesh.new()
821         bm.from_mesh(bpy.context.active_object.data)
822         bm.normal_update()
823
824         bEdges = bm.edges
825         bVerts = bm.verts
826         
827         seg = self.segments
828         edges = [e for e in bEdges if e.select]
829         verts = [edges[v // 2].verts[v % 2] for v in range(4)]
830
831         if self.flip1:
832             v1 = verts[1]
833             p1_co = verts[1].co
834             p1_dir = verts[1].co - verts[0].co
835         else:
836             v1 = verts[0]
837             p1_co = verts[0].co
838             p1_dir = verts[0].co - verts[1].co
839         if self.ten1 < 0:
840             p1_dir = -1 * p1_dir
841             p1_dir.length = -self.ten1
842         else:
843             p1_dir.length = self.ten1
844
845         if self.flip2:
846             v2 = verts[3]
847             p2_co = verts[3].co
848             p2_dir = verts[2].co - verts[3].co
849         else:
850             v2 = verts[2]
851             p2_co = verts[2].co
852             p2_dir = verts[3].co - verts[2].co 
853         if self.ten2 < 0:
854             p2_dir = -1 * p2_dir
855             p2_dir.length = -self.ten2
856         else:
857             p2_dir.length = self.ten2
858
859         # Get the interploted coordinates:
860         if self.alg == 'Blender':
861             pieces = interpolate_bezier(p1_co, p1_dir, p2_dir, p2_co, self.segments)
862         elif self.alg == 'Hermite':
863             pieces = interpolate_line_line(p1_co, p1_dir, p2_co, p2_dir, self.segments, 1, 'HERMITE')
864         elif self.alg == 'Bezier':
865             pieces = interpolate_line_line(p1_co, p1_dir, p2_co, p2_dir, self.segments, 1, 'BEZIER')
866         elif self.alg == 'B-Spline':
867             pieces = interpolate_line_line(p1_co, p1_dir, p2_co, p2_dir, self.segments, 1, 'BSPLINE')
868
869         verts = []
870         verts.append(v1)
871         # Add vertices and set the points:
872         for i in range(seg - 1):
873             v = bVerts.new()
874             v.co = pieces[i]
875             verts.append(v)
876         verts.append(v2)
877         # Connect vertices:
878         for i in range(seg):
879             e = bEdges.new((verts[i], verts[i + 1]))
880
881         bm.to_mesh(bpy.context.active_object.data)
882         bpy.ops.object.editmode_toggle()
883         return {'FINISHED'}
884
885
886 # Creates edges normal to planes defined between each of two edges and the
887 # normal or the plane defined by those two edges.
888 #   - Select two edges.  The must form a plane.
889 #   - On running the script, eight edges will be created.  Delete the
890 #     extras that you don't need.
891 #   - The length of those edges is defined by the variable "length"
892 #
893 # @todo Change method from a cross product to a rotation matrix to make the
894 #   angle part work.
895 #   --- todo completed Feb 4th, but still needs work ---
896 # @todo Figure out a way to make +/- predictable
897 #   - Maybe use angel between edges and vector direction definition?
898 #   --- TODO COMPLETED ON 2/9/2012 ---
899 class Ortho(bpy.types.Operator):
900     bl_idname = "mesh.edgetools_ortho"
901     bl_label = "Angle Off Edge"
902     bl_description = ""
903     bl_options = {'REGISTER', 'UNDO'}
904
905     vert1 = BoolProperty(name = "Vertice 1",
906                          description = "Enable edge creation for vertice 1.",
907                          default = True)
908     vert2 = BoolProperty(name = "Vertice 2",
909                          description = "Enable edge creation for vertice 2.",
910                          default = True)
911     vert3 = BoolProperty(name = "Vertice 3",
912                          description = "Enable edge creation for vertice 3.",
913                          default = True)
914     vert4 = BoolProperty(name = "Vertice 4",
915                          description = "Enable edge creation for vertice 4.",
916                          default = True)
917     pos = BoolProperty(name = "+",
918                        description = "Enable positive direction edges.",
919                        default = True)
920     neg = BoolProperty(name = "-",
921                        description = "Enable negitive direction edges.",
922                        default = True)
923     angle = FloatProperty(name = "Angle",
924                           description = "Angle off of the originating edge",
925                           min = 0.0, max = 180.0,
926                           default = 90.0)
927     length = FloatProperty(name = "Length",
928                            description = "Length of created edges.",
929                            min = 0.0, max = 1024.0,
930                            default = 1.0)
931
932     # For when only one edge is selected (Possible feature to be testd):
933     plane = EnumProperty(name = "Plane",
934                          items = [("XY", "X-Y Plane", "Use the X-Y plane as the plane of creation"),
935                                   ("XZ", "X-Z Plane", "Use the X-Z plane as the plane of creation"),
936                                   ("YZ", "Y-Z Plane", "Use the Y-Z plane as the plane of creation")],
937                          default = "XY")
938
939     def draw(self, context):
940         layout = self.layout
941
942         layout.prop(self, "vert1")
943         layout.prop(self, "vert2")
944         layout.prop(self, "vert3")
945         layout.prop(self, "vert4")
946         row = layout.row(align = False)
947         row.alignment = 'EXPAND'
948         row.prop(self, "pos")
949         row.prop(self, "neg")
950         layout.prop(self, "angle")
951         layout.prop(self, "length")
952     
953     @classmethod
954     def poll(cls, context):
955         ob = context.active_object
956         return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
957
958
959     def invoke(self, context, event):
960         return self.execute(context)
961
962     
963     def execute(self, context):
964         bpy.ops.object.editmode_toggle()
965         bm = bmesh.new()
966         bm.from_mesh(bpy.context.active_object.data)
967         bm.normal_update()
968
969         bVerts = bm.verts
970         bEdges = bm.edges
971         edges = [e for e in bEdges if e.select]
972         vectors = []
973
974         # Until I can figure out a better way of handeling it:
975         if len(edges) < 2:
976             bpy.ops.object.editmode_toggle()
977             self.report({'ERROR_INVALID_INPUT'},
978                         "You must select two edges.")
979             return {'CANCELLED'}
980
981         verts = [edges[0].verts[0],
982                  edges[0].verts[1],
983                  edges[1].verts[0],
984                  edges[1].verts[1]]
985
986         cos = intersect_line_line(verts[0].co, verts[1].co, verts[2].co, verts[3].co)
987
988         # If the two edges are parallel:
989         if cos == None:
990             self.report({'WARNING'},
991                         "Selected lines are parallel: results may be unpredictable.")
992             vectors.append(verts[0].co - verts[1].co)
993             vectors.append(verts[0].co - verts[2].co)
994             vectors.append(vectors[0].cross(vectors[1]))
995             vectors.append(vectors[2].cross(vectors[0]))
996             vectors.append(-vectors[3])
997         else:
998             # Warn the user if they have not chosen two planar edges:
999             if not is_same_co(cos[0], cos[1]):
1000                 self.report({'WARNING'},
1001                             "Selected lines are not planar: results may be unpredictable.")
1002
1003             # This makes the +/- behavior predictable:
1004             if (verts[0].co - cos[0]).length < (verts[1].co - cos[0]).length:
1005                 verts[0], verts[1] = verts[1], verts[0]
1006             if (verts[2].co - cos[0]).length < (verts[3].co - cos[0]).length:
1007                 verts[2], verts[3] = verts[3], verts[2]
1008
1009             vectors.append(verts[0].co - verts[1].co)
1010             vectors.append(verts[2].co - verts[3].co)
1011             
1012             # Normal of the plane formed by vector1 and vector2:
1013             vectors.append(vectors[0].cross(vectors[1]))
1014
1015             # Possible directions:
1016             vectors.append(vectors[2].cross(vectors[0]))
1017             vectors.append(vectors[1].cross(vectors[2]))
1018
1019         # Set the length:
1020         vectors[3].length = self.length
1021         vectors[4].length = self.length
1022
1023         # Perform any additional rotations:
1024         matrix = Matrix.Rotation(radians(90 + self.angle), 3, vectors[2])
1025         vectors.append(matrix * -vectors[3]) # vectors[5]
1026         matrix = Matrix.Rotation(radians(90 - self.angle), 3, vectors[2])
1027         vectors.append(matrix * vectors[4]) # vectors[6]
1028         vectors.append(matrix * vectors[3]) # vectors[7]
1029         matrix = Matrix.Rotation(radians(90 + self.angle), 3, vectors[2])
1030         vectors.append(matrix * -vectors[4]) # vectors[8]
1031
1032         # Perform extrusions and displacements:
1033         # There will be a total of 8 extrusions.  One for each vert of each edge.
1034         # It looks like an extrusion will add the new vert to the end of the verts
1035         # list and leave the rest in the same location.
1036         # ----------- EDIT -----------
1037         # It looks like I might be able to do this within "bpy.data" with the ".add"
1038         # function.
1039         # ------- BMESH UPDATE -------
1040         # BMesh uses ".new()"
1041
1042         for v in range(len(verts)):
1043             vert = verts[v]
1044             if (v == 0 and self.vert1) or (v == 1 and self.vert2) or (v == 2 and self.vert3) or (v == 3 and self.vert4):
1045                 if self.pos:
1046                     new = bVerts.new()
1047                     new.co = vert.co - vectors[5 + (v // 2) + ((v % 2) * 2)]
1048                     bEdges.new((vert, new))
1049                 if self.neg:
1050                     new = bVerts.new()
1051                     new.co = vert.co + vectors[5 + (v // 2) + ((v % 2) * 2)]
1052                     bEdges.new((vert, new))
1053
1054         bm.to_mesh(bpy.context.active_object.data)
1055         bpy.ops.object.editmode_toggle()
1056         return {'FINISHED'}
1057
1058
1059 # Usage:
1060 # Select an edge and a point or an edge and specify the radius (default is 1 BU)
1061 # You can select two edges but it might be unpredicatble which edge it revolves
1062 # around so you might have to play with the switch.
1063 class Shaft(bpy.types.Operator):
1064     bl_idname = "mesh.edgetools_shaft"
1065     bl_label = "Shaft"
1066     bl_description = "Create a shaft mesh around an axis"
1067     bl_options = {'REGISTER', 'UNDO'}
1068
1069     # Selection defaults:
1070     shaftType = 0
1071
1072     # For tracking if the user has changed selection:
1073     last_edge = IntProperty(name = "Last Edge",
1074                             description = "Tracks if user has changed selected edge",
1075                             min = 0, max = 1,
1076                             default = 0)
1077     last_flip = False
1078     
1079     edge = IntProperty(name = "Edge",
1080                        description = "Edge to shaft around.",
1081                        min = 0, max = 1,
1082                        default = 0)
1083     flip = BoolProperty(name = "Flip Second Edge",
1084                         description = "Flip the percieved direction of the second edge.",
1085                         default = False)
1086     radius = FloatProperty(name = "Radius",
1087                            description = "Shaft Radius",
1088                            min = 0.0, max = 1024.0,
1089                            default = 1.0)
1090     start = FloatProperty(name = "Starting Angle",
1091                           description = "Angle to start the shaft at.",
1092                           min = -360.0, max = 360.0,
1093                           default = 0.0)
1094     finish = FloatProperty(name = "Ending Angle",
1095                            description = "Angle to end the shaft at.",
1096                            min = -360.0, max = 360.0,
1097                            default = 360.0)
1098     segments = IntProperty(name = "Shaft Segments",
1099                            description = "Number of sgements to use in the shaft.",
1100                            min = 1, max = 4096,
1101                            soft_max = 512,
1102                            default = 32)
1103
1104
1105     def draw(self, context):
1106         layout = self.layout
1107
1108         if self.shaftType == 0:
1109             layout.prop(self, "edge")
1110             layout.prop(self, "flip")
1111         elif self.shaftType == 3:
1112             layout.prop(self, "radius")
1113         layout.prop(self, "segments")
1114         layout.prop(self, "start")
1115         layout.prop(self, "finish")
1116
1117
1118     @classmethod
1119     def poll(cls, context):
1120         ob = context.active_object
1121         return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
1122
1123
1124     def invoke(self, context, event):
1125         # Make sure these get reset each time we run:
1126         self.last_edge = 0
1127         self.edge = 0
1128
1129         return self.execute(context)
1130
1131     
1132     def execute(self, context):
1133         bpy.ops.object.editmode_toggle()
1134         bm = bmesh.new()
1135         bm.from_mesh(bpy.context.active_object.data)
1136         bm.normal_update()
1137
1138         bFaces = bm.faces
1139         bEdges = bm.edges
1140         bVerts = bm.verts
1141
1142         active = None
1143         edges = []
1144         verts = []
1145
1146         # Pre-caclulated values:
1147
1148         rotRange = [radians(self.start), radians(self.finish)]
1149         rads = radians((self.finish - self.start) / self.segments)
1150
1151         numV = self.segments + 1
1152         numE = self.segments
1153
1154         edges = [e for e in bEdges if e.select]
1155
1156         # Robustness check: there should at least be one edge selected
1157         if len(edges) < 1:
1158             bpy.ops.object.editmode_toggle()
1159             self.report({'ERROR_INVALID_INPUT'},
1160                         "At least one edge must be selected.")
1161             return {'CANCELLED'}
1162
1163         # If two edges are selected:
1164         if len(edges) == 2:
1165             # default:
1166             edge = [0, 1]
1167             vert = [0, 1]
1168
1169             # Edge selection:
1170             #
1171             # By default, we want to shaft around the last selected edge (it
1172             # will be the active edge).  We know we are using the default if
1173             # the user has not changed which edge is being shafted around (as
1174             # is tracked by self.last_edge).  When they are not the same, then
1175             # the user has changed selection.
1176             #
1177             # We then need to make sure that the active object really is an edge
1178             # (robustness check).
1179             #
1180             # Finally, if the active edge is not the inital one, we flip them
1181             # and have the GUI reflect that.
1182             if self.last_edge == self.edge:
1183                 if isinstance(bm.select_history.active, bmesh.types.BMEdge):
1184                     if bm.select_history.active != edges[edge[0]]:
1185                         self.last_edge, self.edge = edge[1], edge[1]
1186                         edge = [edge[1], edge[0]]
1187                 else:
1188                     bpy.ops.object.editmode_toggle()
1189                     self.report({'ERROR_INVALID_INPUT'},
1190                                 "Active geometry is not an edge.")
1191                     return {'CANCELLED'}
1192             elif self.edge == 1:
1193                 edge = [1, 0]
1194                     
1195             verts.append(edges[edge[0]].verts[0])
1196             verts.append(edges[edge[0]].verts[1])
1197
1198             if self.flip:
1199                 verts = [1, 0]
1200
1201             verts.append(edges[edge[1]].verts[vert[0]])
1202             verts.append(edges[edge[1]].verts[vert[1]])
1203
1204             self.shaftType = 0
1205         # If there is more than one edge selected:
1206         # There are some issues with it ATM, so don't expose is it to normal users:
1207         elif len(edges) > 2 and bpy.app.debug:
1208             if isinstance(bm.select_history.active, bmesh.types.BMEdge):
1209                 active = bm.select_history.active
1210                 edges.remove(active)
1211                 # Get all the verts:
1212                 edges = order_joined_edges(edges[0])
1213                 verts = []
1214                 for e in edges:
1215                     if verts.count(e.verts[0]) == 0:
1216                         verts.append(e.verts[0])
1217                     if verts.count(e.verts[1]) == 0:
1218                         verts.append(e.verts[1])
1219             else:
1220                 bpy.ops.object.editmode_toggle()
1221                 self.report({'ERROR_INVALID_INPUT'},
1222                             "Active geometry is not an edge.")
1223                 return {'CANCELLED'}
1224             self.shaftType = 1
1225         else:
1226             verts.append(edges[0].verts[0])
1227             verts.append(edges[0].verts[1])
1228
1229             for v in bVerts:
1230                 if v.select and verts.count(v) == 0:
1231                     verts.append(v)
1232                 v.select = False
1233             if len(verts) == 2:
1234                 self.shaftType = 3
1235             else:
1236                 self.shaftType = 2
1237
1238         # The vector denoting the axis of rotation:
1239         if self.shaftType == 1:
1240             axis = active.verts[1].co - active.verts[0].co
1241         else:
1242             axis = verts[1].co - verts[0].co
1243
1244         # We will need a series of rotation matrices.  We could use one which would be
1245         # faster but also might cause propagation of error.
1246 ##        matrices = []
1247 ##        for i in range(numV):
1248 ##            matrices.append(Matrix.Rotation((rads * i) + rotRange[0], 3, axis))
1249         matrices = [Matrix.Rotation((rads * i) + rotRange[0], 3, axis) for i in range(numV)]
1250
1251         # New vertice coordinates:
1252         verts_out = []
1253
1254         # If two edges were selected:
1255         #   - If the lines are not parallel, then it will create a cone-like shaft
1256         if self.shaftType == 0:
1257             for i in range(len(verts) - 2):
1258                 init_vec = distance_point_line(verts[i + 2].co, verts[0].co, verts[1].co)
1259                 co = init_vec + verts[i + 2].co
1260                 # These will be rotated about the orgin so will need to be shifted:
1261                 for j in range(numV):
1262                     verts_out.append(co - (matrices[j] * init_vec))
1263         elif self.shaftType == 1:
1264             for i in verts:
1265                 init_vec = distance_point_line(i.co, active.verts[0].co, active.verts[1].co)
1266                 co = init_vec + i.co
1267                 # These will be rotated about the orgin so will need to be shifted:
1268                 for j in range(numV):
1269                     verts_out.append(co - (matrices[j] * init_vec))
1270         # Else if a line and a point was selected:    
1271         elif self.shaftType == 2:
1272             init_vec = distance_point_line(verts[2].co, verts[0].co, verts[1].co)
1273             # These will be rotated about the orgin so will need to be shifted:
1274             verts_out = [(verts[i].co - (matrices[j] * init_vec)) for i in range(2) for j in range(numV)]
1275         # Else the above are not possible, so we will just use the edge:
1276         #   - The vector defined by the edge is the normal of the plane for the shaft
1277         #   - The shaft will have radius "radius".
1278         else:
1279             if is_axial(verts[0].co, verts[1].co) == None:
1280                 proj = (verts[1].co - verts[0].co)
1281                 proj[2] = 0
1282                 norm = proj.cross(verts[1].co - verts[0].co)
1283                 vec = norm.cross(verts[1].co - verts[0].co)
1284                 vec.length = self.radius
1285             elif is_axial(verts[0].co, verts[1].co) == 'Z':
1286                 vec = verts[0].co + Vector((0, 0, self.radius))
1287             else:
1288                 vec = verts[0].co + Vector((0, self.radius, 0))
1289             init_vec = distance_point_line(vec, verts[0].co, verts[1].co)
1290             # These will be rotated about the orgin so will need to be shifted:
1291             verts_out = [(verts[i].co - (matrices[j] * init_vec)) for i in range(2) for j in range(numV)]
1292
1293         # We should have the coordinates for a bunch of new verts.  Now add the verts
1294         # and build the edges and then the faces.
1295
1296         newVerts = []
1297
1298         if self.shaftType == 1:
1299             # Vertices:
1300             for i in range(numV * len(verts)):
1301                 new = bVerts.new()
1302                 new.co = verts_out[i]
1303                 new.select = True
1304                 newVerts.append(new)
1305
1306             # Edges:
1307             for i in range(numE):
1308                 for j in range(len(verts)):
1309                     e = bEdges.new((newVerts[i + (numV * j)], newVerts[i + (numV * j) + 1]))
1310                     e.select = True
1311             for i in range(numV):
1312                 for j in range(len(verts) - 1):
1313                     e = bEdges.new((newVerts[i + (numV * j)], newVerts[i + (numV * (j + 1))]))
1314                     e.select = True
1315
1316             # Faces:
1317             # There is a problem with this right now:
1318             for i in range(len(edges)):
1319                 for j in range(numE):
1320                     f = bFaces.new((newVerts[i], newVerts[i + 1],
1321                                     newVerts[i + (numV * j) + 1], newVerts[i + (numV * j)]))
1322                     f.normal_update()
1323         else:
1324             # Vertices:
1325             for i in range(numV * 2):
1326                 new = bVerts.new()
1327                 new.co = verts_out[i]
1328                 new.select = True
1329                 newVerts.append(new)
1330
1331             # Edges:
1332             for i in range(numE):
1333                 e = bEdges.new((newVerts[i], newVerts[i + 1]))
1334                 e.select = True
1335                 e = bEdges.new((newVerts[i + numV], newVerts[i + numV + 1]))
1336                 e.select = True
1337             for i in range(numV):
1338                 e = bEdges.new((newVerts[i], newVerts[i + numV]))
1339                 e.select = True
1340
1341             # Faces:
1342             for i in range(numE):
1343                 f = bFaces.new((newVerts[i], newVerts[i + 1],
1344                                 newVerts[i + numV + 1], newVerts[i + numV]))
1345                 f.normal_update()
1346
1347         bm.to_mesh(bpy.context.active_object.data)
1348         bpy.ops.object.editmode_toggle()
1349         return {'FINISHED'}
1350
1351
1352 # "Slices" edges crossing a plane defined by a face.
1353 class Slice(bpy.types.Operator):
1354     bl_idname = "mesh.edgetools_slice"
1355     bl_label = "Slice"
1356     bl_description = "Cuts edges at the plane defined by a selected face."
1357     bl_options = {'REGISTER', 'UNDO'}
1358
1359     make_copy = BoolProperty(name = "Make Copy",
1360                              description = "Make new vertices at intersection points instead of spliting the edge",
1361                              default = False)
1362     rip = BoolProperty(name = "Rip",
1363                        description = "Split into two edges that DO NOT share an intersection vertice.",
1364                        default = False)
1365     pos = BoolProperty(name = "Positive",
1366                        description = "Remove the portion on the side of the face normal",
1367                        default = False)
1368     neg = BoolProperty(name = "Negative",
1369                        description = "Remove the portion on the side opposite of the face normal",
1370                        default = False)
1371
1372     def draw(self, context):
1373         layout = self.layout
1374
1375         layout.prop(self, "make_copy")
1376         if not self.make_copy:
1377             layout.prop(self, "rip")
1378             layout.label("Remove Side:")
1379             layout.prop(self, "pos")
1380             layout.prop(self, "neg")
1381
1382
1383     @classmethod
1384     def poll(cls, context):
1385         ob = context.active_object
1386         return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
1387
1388
1389     def invoke(self, context, event):
1390         return self.execute(context)
1391
1392     
1393     def execute(self, context):
1394         bpy.ops.object.editmode_toggle()
1395         bm = bmesh.new()
1396         bm.from_mesh(context.active_object.data)
1397         bm.normal_update()
1398
1399         # For easy access to verts, edges, and faces:
1400         bVerts = bm.verts
1401         bEdges = bm.edges
1402         bFaces = bm.faces
1403
1404         face = None
1405         normal = None
1406
1407         # Find the selected face.  This will provide the plane to project onto:
1408         if isinstance(bm.select_history.active, bmesh.types.BMFace):
1409             face = bm.select_history.active
1410             normal = bm.select_history.active.normal
1411             bm.select_history.active.select = False
1412         else:
1413             for f in bFaces:
1414                 if f.select:
1415                     face = f
1416                     normal = f.normal
1417                     f.select = False
1418                     break
1419
1420         if face == None:
1421             bpy.ops.object.editmode_toggle()
1422             self.report({'ERROR_INVALID_INPUT'},
1423                         "You must select a face as the cutting plane.")
1424             return {'CANCELLED'}
1425         elif len(face.verts) > 4 and not is_face_planar(face):
1426             self.report({'WARNING'},
1427                         "Selected face is an n-gon.  Results may be unpredictable.")
1428
1429         for e in bEdges:
1430             v1 = e.verts[0]
1431             v2 = e.verts[1]
1432             if e.select and (v1 not in face.verts and v2 not in face.verts):
1433                 if len(face.verts) < 5:  # Not an n-gon
1434                     intersection = intersect_line_face(e, face, True)
1435                 else:
1436                     intersection = intersect_line_plane(v1.co, v2.co, face.verts[0].co, normal)
1437
1438                 if intersection != None:
1439                     d1 = distance_point_to_plane(v1.co, face.verts[0].co, normal)
1440                     d2 = distance_point_to_plane(v2.co, face.verts[0].co, normal)
1441                     # If they have different signs, then the edge crosses the plane:
1442                     if abs(d1 + d2) < abs(d1 - d2):
1443                         # Make the first vertice the positive vertice:
1444                         if d1 < d2:
1445                             v2, v1 = v1, v2
1446                         if self.make_copy:
1447                             new = bVerts.new()
1448                             new.co = intersection
1449                         elif self.rip:
1450                             newV1 = bVerts.new()
1451                             newV1.co = intersection
1452                             newV2 = bVerts.new()
1453                             newV2.co = intersection
1454                             newE1 = bEdges.new((v1, newV1))
1455                             newE2 = bEdges.new((v2, newV2))
1456                             bEdges.remove(e)
1457                         else:
1458                             new = list(bmesh.utils.edge_split(e, v1, 0.5))
1459                             new[1].co = intersection
1460                             e.select = False
1461                             new[0].select = False
1462                             if self.pos:
1463                                 bEdges.remove(new[0])
1464                             if self.neg:
1465                                 bEdges.remove(e)
1466
1467         bm.to_mesh(context.active_object.data)
1468         bpy.ops.object.editmode_toggle()
1469         return {'FINISHED'}
1470
1471
1472 class Project(bpy.types.Operator):
1473     bl_idname = "mesh.edgetools_project"
1474     bl_label = "Project"
1475     bl_description = "Projects the selected vertices/edges onto the selected plane."
1476     bl_options = {'REGISTER', 'UNDO'}
1477
1478     make_copy = BoolProperty(name = "Make Copy",
1479                              description = "Make a duplicate of the vertices instead of moving it",
1480                              default = False)
1481
1482     def draw(self, context):
1483         layout = self.layout
1484         layout.prop(self, "make_copy")
1485
1486     @classmethod
1487     def poll(cls, context):
1488         ob = context.active_object
1489         return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
1490
1491
1492     def invoke(self, context, event):
1493         return self.execute(context)
1494
1495
1496     def execute(self, context):
1497         bpy.ops.object.editmode_toggle()
1498         bm = bmesh.new()
1499         bm.from_mesh(context.active_object.data)
1500         bm.normal_update()
1501
1502         bFaces = bm.faces
1503         bEdges = bm.edges
1504         bVerts = bm.verts
1505
1506         fVerts = []
1507
1508         # Find the selected face.  This will provide the plane to project onto:
1509         for f in bFaces:
1510             if f.select:
1511                 for v in f.verts:
1512                     fVerts.append(v)
1513                 normal = f.normal
1514                 f.select = False
1515                 break
1516
1517         for v in bVerts:
1518             if v.select:
1519                 if v in fVerts:
1520                     v.select = False
1521                     continue
1522                 d = distance_point_to_plane(v.co, fVerts[0].co, normal)
1523                 if self.make_copy:
1524                     temp = v
1525                     v = bVerts.new()
1526                     v.co = temp.co
1527                 vector = normal
1528                 vector.length = abs(d)
1529                 v.co = v.co - (vector * sign(d))
1530                 v.select = False
1531
1532         bm.to_mesh(context.active_object.data)
1533         bpy.ops.object.editmode_toggle()
1534         return {'FINISHED'}
1535
1536
1537 # Project_End is for projecting/extending an edge to meet a plane.
1538 # This is used be selecting a face to define the plane then all the edges.
1539 # The add-on will then move the vertices in the edge that is closest to the
1540 # plane to the coordinates of the intersection of the edge and the plane.
1541 class Project_End(bpy.types.Operator):
1542     bl_idname = "mesh.edgetools_project_end"
1543     bl_label = "Project (End Point)"
1544     bl_description = "Projects the vertice of the selected edges closest to a plane onto that plane."
1545     bl_options = {'REGISTER', 'UNDO'}
1546
1547     make_copy = BoolProperty(name = "Make Copy",
1548                              description = "Make a duplicate of the vertice instead of moving it",
1549                              default = False)
1550     keep_length = BoolProperty(name = "Keep Edge Length",
1551                                description = "Maintain edge lengths",
1552                                default = False)
1553     use_force = BoolProperty(name = "Use opposite vertices",
1554                              description = "Force the usage of the vertices at the other end of the edge",
1555                              default = False)
1556     use_normal = BoolProperty(name = "Project along normal",
1557                               description = "Use the plane's normal as the projection direction",
1558                               default = False)
1559
1560     def draw(self, context):
1561         layout = self.layout
1562 ##        layout.prop(self, "keep_length")
1563         if not self.keep_length:
1564             layout.prop(self, "use_normal")
1565 ##        else:
1566 ##            self.report({'ERROR_INVALID_INPUT'}, "Maintaining edge length not yet supported")
1567 ##            self.report({'WARNING'}, "Projection may result in unexpected geometry")
1568         layout.prop(self, "make_copy")
1569         layout.prop(self, "use_force")
1570
1571
1572     @classmethod
1573     def poll(cls, context):
1574         ob = context.active_object
1575         return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
1576
1577
1578     def invoke(self, context, event):
1579         return self.execute(context)
1580
1581
1582     def execute(self, context):
1583         bpy.ops.object.editmode_toggle()
1584         bm = bmesh.new()
1585         bm.from_mesh(context.active_object.data)
1586         bm.normal_update()
1587
1588         bFaces = bm.faces
1589         bEdges = bm.edges
1590         bVerts = bm.verts
1591
1592         fVerts = []
1593
1594         # Find the selected face.  This will provide the plane to project onto:
1595         for f in bFaces:
1596             if f.select:
1597                 for v in f.verts:
1598                     fVerts.append(v)
1599                 normal = f.normal
1600                 f.select = False
1601                 break
1602
1603         for e in bEdges:
1604             if e.select:
1605                 v1 = e.verts[0]
1606                 v2 = e.verts[1]
1607                 if v1 in fVerts or v2 in fVerts:
1608                     e.select = False
1609                     continue
1610                 intersection = intersect_line_plane(v1.co, v2.co, fVerts[0].co, normal)
1611                 if intersection != None:
1612                     # Use abs because we don't care what side of plane we're on:
1613                     d1 = distance_point_to_plane(v1.co, fVerts[0].co, normal)
1614                     d2 = distance_point_to_plane(v2.co, fVerts[0].co, normal)
1615                     # If d1 is closer than we use v1 as our vertice:
1616                     # "xor" with 'use_force':
1617                     if (abs(d1) < abs(d2)) is not self.use_force:
1618                         if self.make_copy:
1619                             v1 = bVerts.new()
1620                             v1.co = e.verts[0].co
1621                         if self.keep_length:
1622                             v1.co = intersection
1623                         elif self.use_normal:
1624                             vector = normal
1625                             vector.length = abs(d1)
1626                             v1.co = v1.co - (vector * sign(d1))
1627                         else:
1628                             v1.co = intersection
1629                     else:
1630                         if self.make_copy:
1631                             v2 = bVerts.new()
1632                             v2.co = e.verts[1].co
1633                         if self.keep_length:
1634                             v2.co = intersection
1635                         elif self.use_normal:
1636                             vector = normal
1637                             vector.length = abs(d2)
1638                             v2.co = v2.co - (vector * sign(d2))
1639                         else:
1640                             v2.co = intersection
1641                 e.select = False
1642
1643         bm.to_mesh(context.active_object.data)
1644         bpy.ops.object.editmode_toggle()
1645         return {'FINISHED'}
1646
1647
1648 # Edge Fillet
1649 #
1650 # Blender currently does not have a CAD-style edge-based fillet function. This
1651 # is my atempt to create one.  It should take advantage of BMesh and the ngon
1652 # capabilities for non-destructive modeling, if possible.  This very well may
1653 # not result in nice quads and it will be up to the artist to clean up the mesh
1654 # back into quads if necessary.
1655 #
1656 # Assumptions:
1657 #   - Faces are planar. This should, however, do a check an warn otherwise.
1658 #
1659 # Developement Process:
1660 # Because this will eventaully prove to be a great big jumble of code and
1661 # various functionality, this is to provide an outline for the developement
1662 # and functionality wanted at each milestone.
1663 #   1) intersect_line_face: function to find the intersection point, if it
1664 #       exists, at which a line intersects a face.  The face does not have to
1665 #       be planar, and can be an ngon.  This will allow for a point to be placed
1666 #       on the actual mesh-face for non-planar faces.
1667 #   2) Minimal propagation, single edge: Filleting of a single edge without
1668 #       propagation of the fillet along "tangent" edges.
1669 #   3) Minimal propagation, multiple edges: Perform said fillet along/on
1670 #       multiple edges.
1671 #   4) "Tangency" detection code: because we have a mesh based geometry, this
1672 #       have to make an educated guess at what is actually supposed to be
1673 #       treated as tangent and what constitutes a sharp edge.  This should
1674 #       respect edges marked as sharp (does not propagate passed an
1675 #       intersecting edge that is marked as sharp).
1676 #   5) Tangent propagation, single edge: Filleting of a single edge using the
1677 #       above tangency detection code to continue the fillet to adjacent
1678 #       "tangent" edges.
1679 #   6) Tangent propagation, multiple edges: Same as above, but with multiple
1680 #       edges selected.  If multiple edges were selected along the same
1681 #       tangency path, only one edge will be filleted.  The others must be
1682 #       ignored/discarded.
1683 class Fillet(bpy.types.Operator):
1684     bl_idname = "mesh.edgetools_fillet"
1685     bl_label = "Edge Fillet"
1686     bl_description = "Fillet the selected edges."
1687     bl_options = {'REGISTER', 'UNDO'}
1688
1689     radius = FloatProperty(name = "Radius",
1690                            description = "Radius of the edge fillet",
1691                            min = 0.00001, max = 1024.0,
1692                            default = 0.5)
1693     prop = EnumProperty(name = "Propagation",
1694                         items = [("m", "Minimal", "Minimal edge propagation"),
1695                                  ("t", "Tangential", "Tangential edge propagation")],
1696                         default = "m")
1697     prop_fac = FloatProperty(name = "Propagation Factor",
1698                              description = "Corner detection sensitivity factor for tangential propagation",
1699                              min = 0.0, max = 100.0,
1700                              default = 25.0)
1701     deg_seg = FloatProperty(name = "Degrees/Section",
1702                             description = "Approximate degrees per section",
1703                             min = 0.00001, max = 180.0,
1704                             default = 10.0)
1705     res = IntProperty(name = "Resolution",
1706                       description = "Resolution of the fillet",
1707                       min = 1, max = 1024,
1708                       default = 8)
1709
1710     def draw(self, context):
1711         layout = self.layout
1712         layout.prop(self, "radius")
1713         layout.prop(self, "prop")
1714         if self.prop == "t":
1715             layout.prop(self, "prop_fac")
1716         layout.prop(self, "deg_seg")
1717         layout.prop(self, "res")
1718
1719     
1720     @classmethod
1721     def poll(cls, context):
1722         ob = context.active_object
1723         return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
1724
1725
1726     def invoke(self, context, event):
1727         return self.execute(context)
1728
1729
1730     def execute(self, context):
1731         bpy.ops.object.editmode_toggle()
1732         bm = bmesh.new()
1733         bm.from_mesh(bpy.context.active_object.data)
1734         bm.normal_update()
1735
1736         bFaces = bm.faces
1737         bEdges = bm.edges
1738         bVerts = bm.verts
1739
1740         # Robustness check: this does not support n-gons (at least for now)
1741         # because I have no idea how to handle them righ now.  If there is
1742         # an n-gon in the mesh, warn the user that results may be nuts because
1743         # of it.
1744         #
1745         # I'm not going to cause it to exit if there are n-gons, as they may
1746         # not be encountered.
1747         # @todo I would like this to be a confirmation dialoge of some sort
1748         # @todo I would REALLY like this to just handle n-gons. . . .
1749         for f in bFaces:
1750             if len(face.verts) > 4:
1751                 self.report({'WARNING'},
1752                             "Mesh contains n-gons which are not supported. Operation may fail.")
1753                 break
1754
1755         # Get the selected edges:
1756         # Robustness check: boundary and wire edges are not fillet-able.
1757         edges = [e for e in bEdges if e.select and not e.is_boundary and not e.is_wire]
1758
1759         for e in edges:
1760             axis_points = fillet_axis(e, self.radius)
1761             
1762
1763         bm.to_mesh(bpy.context.active_object.data)
1764         bpy.ops.object.editmode_toggle()
1765         return {'FINISHED'}
1766
1767
1768 # For testing the mess that is "intersect_line_face" for possible math errors.
1769 # This will NOT be directly exposed to end users: it will always require running
1770 # Blender in debug mode.
1771 # So far no errors have been found. Thanks to anyone who tests and reports bugs!
1772 class Intersect_Line_Face(bpy.types.Operator):
1773     bl_idname = "mesh.edgetools_ilf"
1774     bl_label = "ILF TEST"
1775     bl_description = "TEST ONLY: INTERSECT_LINE_FACE"
1776     bl_options = {'REGISTER', 'UNDO'}
1777
1778     @classmethod
1779     def poll(cls, context):
1780         ob = context.active_object
1781         return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
1782
1783
1784     def invoke(self, context, event):
1785         return self.execute(context)
1786
1787
1788     def execute(self, context):
1789         # Make sure we really are in debug mode:
1790         if not bpy.app.debug:
1791             self.report({'ERROR_INVALID_INPUT'},
1792                         "This is for debugging only: you should not be able to run this!")
1793             return {'CANCELLED'}
1794         
1795         bpy.ops.object.editmode_toggle()
1796         bm = bmesh.new()
1797         bm.from_mesh(bpy.context.active_object.data)
1798         bm.normal_update()
1799
1800         bFaces = bm.faces
1801         bEdges = bm.edges
1802         bVerts = bm.verts
1803
1804         face = None
1805         for f in bFaces:
1806             if f.select:
1807                 face = f
1808                 break
1809
1810         edge = None
1811         for e in bEdges:
1812             if e.select and not e in face.edges:
1813                 edge = e
1814                 break
1815
1816         point = intersect_line_face(edge, face, True)
1817
1818         if point != None:
1819             new = bVerts.new()
1820             new.co = point
1821         else:
1822             bpy.ops.object.editmode_toggle()
1823             self.report({'ERROR_INVALID_INPUT'}, "point was \"None\"")
1824             return {'CANCELLED'}
1825
1826         bm.to_mesh(bpy.context.active_object.data)
1827         bpy.ops.object.editmode_toggle()
1828         return {'FINISHED'}
1829
1830
1831 class VIEW3D_MT_edit_mesh_edgetools(bpy.types.Menu):
1832     bl_label = "EdgeTools"
1833     
1834     def draw(self, context):
1835         layout = self.layout
1836         
1837         layout.operator("mesh.edgetools_extend")
1838         layout.operator("mesh.edgetools_spline")
1839         layout.operator("mesh.edgetools_ortho")
1840         layout.operator("mesh.edgetools_shaft")
1841         layout.operator("mesh.edgetools_slice")
1842         layout.operator("mesh.edgetools_project")
1843         layout.operator("mesh.edgetools_project_end")
1844         if bpy.app.debug:
1845             ## Not ready for prime-time yet:
1846             layout.operator("mesh.edgetools_fillet")
1847             ## For internal testing ONLY:
1848             layout.operator("mesh.edgetools_ilf")
1849
1850
1851 def menu_func(self, context):
1852     self.layout.menu("VIEW3D_MT_edit_mesh_edgetools")
1853     self.layout.separator()
1854
1855
1856 # define classes for registration
1857 classes = [VIEW3D_MT_edit_mesh_edgetools,
1858     Extend,
1859     Spline,
1860     Ortho,
1861     Shaft,
1862     Slice,
1863     Project,
1864     Project_End,
1865     Fillet,
1866     Intersect_Line_Face]
1867
1868
1869 # registering and menu integration
1870 def register():
1871     if int(bpy.app.build_revision[0:5]) < 44800:
1872         print("Error in Edgetools:")
1873         print("This version of Blender does not support the necessary BMesh API.")
1874         print("Please download Blender 2.63 or newer.")
1875         return {'ERROR'}
1876     for c in classes:
1877         bpy.utils.register_class(c)
1878     bpy.types.VIEW3D_MT_edit_mesh_specials.prepend(menu_func)
1879
1880
1881 # unregistering and removing menus
1882 def unregister():
1883     for c in classes:
1884         bpy.utils.unregister_class(c)
1885     bpy.types.VIEW3D_MT_edit_mesh_specials.remove(menu_func)
1886
1887
1888 if __name__ == "__main__":
1889     register()
1890