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