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