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