Welcome to mirror list, hosted at ThFree Co, Russian Federation.

git.blender.org/blender-addons.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'mesh_tools/mesh_edgetools.py')
-rw-r--r--mesh_tools/mesh_edgetools.py1880
1 files changed, 1880 insertions, 0 deletions
diff --git a/mesh_tools/mesh_edgetools.py b/mesh_tools/mesh_edgetools.py
new file mode 100644
index 00000000..6fd98b75
--- /dev/null
+++ b/mesh_tools/mesh_edgetools.py
@@ -0,0 +1,1880 @@
+# The Blender Edgetools is to bring CAD tools to Blender.
+# Copyright (C) 2012 Paul Marshall
+
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+# <pep8 compliant>
+
+bl_info = {
+ "name": "EdgeTools",
+ "author": "Paul Marshall",
+ "version": (0, 9, 2),
+ "blender": (2, 80, 0),
+ "location": "View3D > Toolbar and View3D > Specials (W-key)",
+ "warning": "",
+ "description": "CAD style edge manipulation tools",
+ "wiki_url": "https://wiki.blender.org/index.php/Extensions:2.6/Py/"
+ "Scripts/Modeling/EdgeTools",
+ "category": "Mesh"}
+
+
+import bpy
+import bmesh
+from bpy.types import (
+ Operator,
+ Menu,
+ )
+from math import acos, pi, radians, sqrt
+from mathutils import Matrix, Vector
+from mathutils.geometry import (
+ distance_point_to_plane,
+ interpolate_bezier,
+ intersect_point_line,
+ intersect_line_line,
+ intersect_line_plane,
+ )
+from bpy.props import (
+ BoolProperty,
+ IntProperty,
+ FloatProperty,
+ EnumProperty,
+ )
+
+"""
+Blender EdgeTools
+This is a toolkit for edge manipulation based on mesh manipulation
+abilities of several CAD/CAE packages, notably CATIA's Geometric Workbench
+from which most of these tools have a functional basis.
+
+The GUI and Blender add-on structure shamelessly coded in imitation of the
+LoopTools addon.
+
+Examples:
+- "Ortho" inspired from CATIA's line creation tool which creates a line of a
+ user specified length at a user specified angle to a curve at a chosen
+ point. The user then selects the plane the line is to be created in.
+- "Shaft" is inspired from CATIA's tool of the same name. However, instead
+ of a curve around an axis, this will instead shaft a line, a point, or
+ a fixed radius about the selected axis.
+- "Slice" is from CATIA's ability to split a curve on a plane. When
+ completed this be a Python equivalent with all the same basic
+ functionality, though it will sadly be a little clumsier to use due
+ to Blender's selection limitations.
+
+Notes:
+- Fillet operator and related functions removed as they didn't work
+- Buggy parts have been hidden behind ENABLE_DEBUG global (set it to True)
+ Example: Shaft with more than two edges selected
+
+Paul "BrikBot" Marshall
+Created: January 28, 2012
+Last Modified: October 6, 2012
+
+Coded in IDLE, tested in Blender 2.6.
+Search for "@todo" to quickly find sections that need work
+
+Note: lijenstina - modified this script in preparation for merging
+fixed the needless jumping to object mode for bmesh creation
+causing the crash with the Slice > Rip operator
+Removed the test operator since version 0.9.2
+added general error handling
+"""
+
+# Enable debug
+# Set to True to have the debug prints available
+ENABLE_DEBUG = False
+
+
+# Quick an dirty method for getting the sign of a number:
+def sign(number):
+ return (number > 0) - (number < 0)
+
+
+# is_parallel
+# Checks to see if two lines are parallel
+
+def is_parallel(v1, v2, v3, v4):
+ result = intersect_line_line(v1, v2, v3, v4)
+ return result is None
+
+
+# Handle error notifications
+def error_handlers(self, op_name, error, reports="ERROR", func=False):
+ if self and reports:
+ self.report({'WARNING'}, reports + " (See Console for more info)")
+
+ is_func = "Function" if func else "Operator"
+ print("\n[Mesh EdgeTools]\n{}: {}\nError: {}\n".format(is_func, op_name, error))
+
+
+def flip_edit_mode():
+ bpy.ops.object.editmode_toggle()
+ bpy.ops.object.editmode_toggle()
+
+
+# check the appropriate selection condition
+# to prevent crashes with the index out of range errors
+# pass the bEdges and bVerts based selection tables here
+# types: Edge, Vertex, All
+def is_selected_enough(self, bEdges, bVerts, edges_n=1, verts_n=0, types="Edge"):
+ check = False
+ try:
+ if bEdges and types == "Edge":
+ check = (len(bEdges) >= edges_n)
+ elif bVerts and types == "Vertex":
+ check = (len(bVerts) >= verts_n)
+ elif bEdges and bVerts and types == "All":
+ check = (len(bEdges) >= edges_n and len(bVerts) >= verts_n)
+
+ if check is False:
+ strings = "%s Vertices and / or " % verts_n if verts_n != 0 else ""
+ self.report({'WARNING'},
+ "Needs at least " + strings + "%s Edge(s) selected. "
+ "Operation Cancelled" % edges_n)
+ flip_edit_mode()
+
+ return check
+
+ except Exception as e:
+ error_handlers(self, "is_selected_enough", e,
+ "No appropriate selection. Operation Cancelled", func=True)
+ return False
+
+ return False
+
+
+# is_axial
+# This is for the special case where the edge is parallel to an axis.
+# The projection onto the XY plane will fail so it will have to be handled differently
+
+def is_axial(v1, v2, error=0.000002):
+ vector = v2 - v1
+ # Don't need to store, but is easier to read:
+ vec0 = vector[0] > -error and vector[0] < error
+ vec1 = vector[1] > -error and vector[1] < error
+ vec2 = vector[2] > -error and vector[2] < error
+ if (vec0 or vec1) and vec2:
+ return 'Z'
+ elif vec0 and vec1:
+ return 'Y'
+ return None
+
+
+# is_same_co
+# For some reason "Vector = Vector" does not seem to look at the actual coordinates
+
+def is_same_co(v1, v2):
+ if len(v1) != len(v2):
+ return False
+ else:
+ for co1, co2 in zip(v1, v2):
+ if co1 != co2:
+ return False
+ return True
+
+
+def is_face_planar(face, error=0.0005):
+ for v in face.verts:
+ d = distance_point_to_plane(v.co, face.verts[0].co, face.normal)
+ if ENABLE_DEBUG:
+ print("Distance: " + str(d))
+ if d < -error or d > error:
+ return False
+ return True
+
+
+# other_joined_edges
+# Starts with an edge. Then scans for linked, selected edges and builds a
+# list with them in "order", starting at one end and moving towards the other
+
+def order_joined_edges(edge, edges=[], direction=1):
+ if len(edges) == 0:
+ edges.append(edge)
+ edges[0] = edge
+
+ if ENABLE_DEBUG:
+ print(edge, end=", ")
+ print(edges, end=", ")
+ print(direction, end="; ")
+
+ # Robustness check: direction cannot be zero
+ if direction == 0:
+ direction = 1
+
+ newList = []
+ for e in edge.verts[0].link_edges:
+ if e.select and edges.count(e) == 0:
+ if direction > 0:
+ edges.insert(0, e)
+ newList.extend(order_joined_edges(e, edges, direction + 1))
+ newList.extend(edges)
+ else:
+ edges.append(e)
+ newList.extend(edges)
+ newList.extend(order_joined_edges(e, edges, direction - 1))
+
+ # This will only matter at the first level:
+ direction = direction * -1
+
+ for e in edge.verts[1].link_edges:
+ if e.select and edges.count(e) == 0:
+ if direction > 0:
+ edges.insert(0, e)
+ newList.extend(order_joined_edges(e, edges, direction + 2))
+ newList.extend(edges)
+ else:
+ edges.append(e)
+ newList.extend(edges)
+ newList.extend(order_joined_edges(e, edges, direction))
+
+ if ENABLE_DEBUG:
+ print(newList, end=", ")
+ print(direction)
+
+ return newList
+
+
+# --------------- GEOMETRY CALCULATION METHODS --------------
+
+# distance_point_line
+# I don't know why the mathutils.geometry API does not already have this, but
+# it is trivial to code using the structures already in place. Instead of
+# returning a float, I also want to know the direction vector defining the
+# distance. Distance can be found with "Vector.length"
+
+def distance_point_line(pt, line_p1, line_p2):
+ int_co = intersect_point_line(pt, line_p1, line_p2)
+ distance_vector = int_co[0] - pt
+ return distance_vector
+
+
+# interpolate_line_line
+# This is an experiment into a cubic Hermite spline (c-spline) for connecting
+# two edges with edges that obey the general equation.
+# This will return a set of point coordinates (Vectors)
+#
+# A good, easy to read background on the mathematics can be found at:
+# http://cubic.org/docs/hermite.htm
+#
+# Right now this is . . . less than functional :P
+# @todo
+# - C-Spline and Bezier curves do not end on p2_co as they are supposed to.
+# - B-Spline just fails. Epically.
+# - Add more methods as I come across them. Who said flexibility was bad?
+
+def interpolate_line_line(p1_co, p1_dir, p2_co, p2_dir, segments, tension=1,
+ typ='BEZIER', include_ends=False):
+ pieces = []
+ fraction = 1 / segments
+
+ # Form: p1, tangent 1, p2, tangent 2
+ if typ == 'HERMITE':
+ poly = [[2, -3, 0, 1], [1, -2, 1, 0],
+ [-2, 3, 0, 0], [1, -1, 0, 0]]
+ elif typ == 'BEZIER':
+ poly = [[-1, 3, -3, 1], [3, -6, 3, 0],
+ [1, 0, 0, 0], [-3, 3, 0, 0]]
+ p1_dir = p1_dir + p1_co
+ p2_dir = -p2_dir + p2_co
+ elif typ == 'BSPLINE':
+ # Supposed poly matrix for a cubic b-spline:
+ # poly = [[-1, 3, -3, 1], [3, -6, 3, 0],
+ # [-3, 0, 3, 0], [1, 4, 1, 0]]
+ # My own invention to try to get something that somewhat acts right
+ # This is semi-quadratic rather than fully cubic:
+ poly = [[0, -1, 0, 1], [1, -2, 1, 0],
+ [0, -1, 2, 0], [1, -1, 0, 0]]
+
+ if include_ends:
+ pieces.append(p1_co)
+
+ # Generate each point:
+ for i in range(segments - 1):
+ t = fraction * (i + 1)
+ if ENABLE_DEBUG:
+ print(t)
+ s = [t ** 3, t ** 2, t, 1]
+ h00 = (poly[0][0] * s[0]) + (poly[0][1] * s[1]) + (poly[0][2] * s[2]) + (poly[0][3] * s[3])
+ h01 = (poly[1][0] * s[0]) + (poly[1][1] * s[1]) + (poly[1][2] * s[2]) + (poly[1][3] * s[3])
+ h10 = (poly[2][0] * s[0]) + (poly[2][1] * s[1]) + (poly[2][2] * s[2]) + (poly[2][3] * s[3])
+ h11 = (poly[3][0] * s[0]) + (poly[3][1] * s[1]) + (poly[3][2] * s[2]) + (poly[3][3] * s[3])
+ pieces.append((h00 * p1_co) + (h01 * p1_dir) + (h10 * p2_co) + (h11 * p2_dir))
+ if include_ends:
+ pieces.append(p2_co)
+
+ # Return:
+ if len(pieces) == 0:
+ return None
+ else:
+ if ENABLE_DEBUG:
+ print(pieces)
+ return pieces
+
+
+# intersect_line_face
+
+# Calculates the coordinate of intersection of a line with a face. It returns
+# the coordinate if one exists, otherwise None. It can only deal with tris or
+# quads for a face. A quad does NOT have to be planar
+"""
+Quad math and theory:
+A quad may not be planar. Therefore the treated definition of the surface is
+that the surface is composed of all lines bridging two other lines defined by
+the given four points. The lines do not "cross"
+
+The two lines in 3-space can defined as:
+┌ ┐ ┌ ┐ ┌ ┐ ┌ ┐ ┌ ┐ ┌ ┐
+│x1│ │a11│ │b11│ │x2│ │a21│ │b21│
+│y1│ = (1-t1)│a12│ + t1│b12│, │y2│ = (1-t2)│a22│ + t2│b22│
+│z1│ │a13│ │b13│ │z2│ │a23│ │b23│
+└ ┘ └ ┘ └ ┘ └ ┘ └ ┘ └ ┘
+Therefore, the surface is the lines defined by every point alone the two
+lines with a same "t" value (t1 = t2). This is basically R = V1 + tQ, where
+Q = V2 - V1 therefore R = V1 + t(V2 - V1) -> R = (1 - t)V1 + tV2:
+┌ ┐ ┌ ┐ ┌ ┐
+│x12│ │(1-t)a11 + t * b11│ │(1-t)a21 + t * b21│
+│y12│ = (1 - t12)│(1-t)a12 + t * b12│ + t12│(1-t)a22 + t * b22│
+│z12│ │(1-t)a13 + t * b13│ │(1-t)a23 + t * b23│
+└ ┘ └ ┘ └ ┘
+Now, the equation of our line can be likewise defined:
+┌ ┐ ┌ ┐ ┌ ┐
+│x3│ │a31│ │b31│
+│y3│ = │a32│ + t3│b32│
+│z3│ │a33│ │b33│
+└ ┘ └ ┘ └ ┘
+Now we just have to find a valid solution for the two equations. This should
+be our point of intersection. Therefore, x12 = x3 -> x, y12 = y3 -> y,
+z12 = z3 -> z. Thus, to find that point we set the equation defining the
+surface as equal to the equation for the line:
+ ┌ ┐ ┌ ┐ ┌ ┐ ┌ ┐
+ │(1-t)a11 + t * b11│ │(1-t)a21 + t * b21│ │a31│ │b31│
+(1 - t12)│(1-t)a12 + t * b12│ + t12│(1-t)a22 + t * b22│ = │a32│ + t3│b32│
+ │(1-t)a13 + t * b13│ │(1-t)a23 + t * b23│ │a33│ │b33│
+ └ ┘ └ ┘ └ ┘ └ ┘
+This leaves us with three equations, three unknowns. Solving the system by
+hand is practically impossible, but using Mathematica we are given an insane
+series of three equations (not reproduced here for the sake of space: see
+http://www.mediafire.com/file/cc6m6ba3sz2b96m/intersect_line_surface.nb and
+http://www.mediafire.com/file/0egbr5ahg14talm/intersect_line_surface2.nb for
+Mathematica computation).
+
+Additionally, the resulting series of equations may result in a div by zero
+exception if the line in question if parallel to one of the axis or if the
+quad is planar and parallel to either the XY, XZ, or YZ planes. However, the
+system is still solvable but must be dealt with a little differently to avaid
+these special cases. Because the resulting equations are a little different,
+we have to code them differently. 00Hence the special cases.
+
+Tri math and theory:
+A triangle must be planar (three points define a plane). So we just
+have to make sure that the line intersects inside the triangle.
+
+If the point is within the triangle, then the angle between the lines that
+connect the point to the each individual point of the triangle will be
+equal to 2 * PI. Otherwise, if the point is outside the triangle, then the
+sum of the angles will be less.
+"""
+# @todo
+# - Figure out how to deal with n-gons
+# How the heck is a face with 8 verts defined mathematically?
+# How do I then find the intersection point of a line with said vert?
+# How do I know if that point is "inside" all the verts?
+# I have no clue, and haven't been able to find anything on it so far
+# Maybe if someone (actually reads this and) who knows could note?
+
+
+def intersect_line_face(edge, face, is_infinite=False, error=0.000002):
+ int_co = None
+
+ # If we are dealing with a non-planar quad:
+ if len(face.verts) == 4 and not is_face_planar(face):
+ edgeA = face.edges[0]
+ edgeB = None
+ flipB = False
+
+ for i in range(len(face.edges)):
+ if face.edges[i].verts[0] not in edgeA.verts and \
+ face.edges[i].verts[1] not in edgeA.verts:
+
+ edgeB = face.edges[i]
+ break
+
+ # I haven't figured out a way to mix this in with the above. Doing so might remove a
+ # few extra instructions from having to be executed saving a few clock cycles:
+ for i in range(len(face.edges)):
+ if face.edges[i] == edgeA or face.edges[i] == edgeB:
+ continue
+ 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)):
+
+ flipB = True
+ break
+
+ # Define calculation coefficient constants:
+ # "xx1" is the x coordinate, "xx2" is the y coordinate, and "xx3" is the z coordinate
+ a11, a12, a13 = edgeA.verts[0].co[0], edgeA.verts[0].co[1], edgeA.verts[0].co[2]
+ b11, b12, b13 = edgeA.verts[1].co[0], edgeA.verts[1].co[1], edgeA.verts[1].co[2]
+
+ if flipB:
+ a21, a22, a23 = edgeB.verts[1].co[0], edgeB.verts[1].co[1], edgeB.verts[1].co[2]
+ b21, b22, b23 = edgeB.verts[0].co[0], edgeB.verts[0].co[1], edgeB.verts[0].co[2]
+ else:
+ a21, a22, a23 = edgeB.verts[0].co[0], edgeB.verts[0].co[1], edgeB.verts[0].co[2]
+ b21, b22, b23 = edgeB.verts[1].co[0], edgeB.verts[1].co[1], edgeB.verts[1].co[2]
+ a31, a32, a33 = edge.verts[0].co[0], edge.verts[0].co[1], edge.verts[0].co[2]
+ b31, b32, b33 = edge.verts[1].co[0], edge.verts[1].co[1], edge.verts[1].co[2]
+
+ # There are a bunch of duplicate "sub-calculations" inside the resulting
+ # equations for t, t12, and t3. Calculate them once and store them to
+ # reduce computational time:
+ m01 = a13 * a22 * a31
+ m02 = a12 * a23 * a31
+ m03 = a13 * a21 * a32
+ m04 = a11 * a23 * a32
+ m05 = a12 * a21 * a33
+ m06 = a11 * a22 * a33
+ m07 = a23 * a32 * b11
+ m08 = a22 * a33 * b11
+ m09 = a23 * a31 * b12
+ m10 = a21 * a33 * b12
+ m11 = a22 * a31 * b13
+ m12 = a21 * a32 * b13
+ m13 = a13 * a32 * b21
+ m14 = a12 * a33 * b21
+ m15 = a13 * a31 * b22
+ m16 = a11 * a33 * b22
+ m17 = a12 * a31 * b23
+ m18 = a11 * a32 * b23
+ m19 = a13 * a22 * b31
+ m20 = a12 * a23 * b31
+ m21 = a13 * a32 * b31
+ m22 = a23 * a32 * b31
+ m23 = a12 * a33 * b31
+ m24 = a22 * a33 * b31
+ m25 = a23 * b12 * b31
+ m26 = a33 * b12 * b31
+ m27 = a22 * b13 * b31
+ m28 = a32 * b13 * b31
+ m29 = a13 * b22 * b31
+ m30 = a33 * b22 * b31
+ m31 = a12 * b23 * b31
+ m32 = a32 * b23 * b31
+ m33 = a13 * a21 * b32
+ m34 = a11 * a23 * b32
+ m35 = a13 * a31 * b32
+ m36 = a23 * a31 * b32
+ m37 = a11 * a33 * b32
+ m38 = a21 * a33 * b32
+ m39 = a23 * b11 * b32
+ m40 = a33 * b11 * b32
+ m41 = a21 * b13 * b32
+ m42 = a31 * b13 * b32
+ m43 = a13 * b21 * b32
+ m44 = a33 * b21 * b32
+ m45 = a11 * b23 * b32
+ m46 = a31 * b23 * b32
+ m47 = a12 * a21 * b33
+ m48 = a11 * a22 * b33
+ m49 = a12 * a31 * b33
+ m50 = a22 * a31 * b33
+ m51 = a11 * a32 * b33
+ m52 = a21 * a32 * b33
+ m53 = a22 * b11 * b33
+ m54 = a32 * b11 * b33
+ m55 = a21 * b12 * b33
+ m56 = a31 * b12 * b33
+ m57 = a12 * b21 * b33
+ m58 = a32 * b21 * b33
+ m59 = a11 * b22 * b33
+ m60 = a31 * b22 * b33
+ m61 = a33 * b12 * b21
+ m62 = a32 * b13 * b21
+ m63 = a33 * b11 * b22
+ m64 = a31 * b13 * b22
+ m65 = a32 * b11 * b23
+ m66 = a31 * b12 * b23
+ m67 = b13 * b22 * b31
+ m68 = b12 * b23 * b31
+ m69 = b13 * b21 * b32
+ m70 = b11 * b23 * b32
+ m71 = b12 * b21 * b33
+ m72 = b11 * b22 * b33
+ n01 = m01 - m02 - m03 + m04 + m05 - m06
+ 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
+ n03 = -m19 + m20 + m33 - m34 - m47 + m48
+ n04 = m21 - m22 - m23 + m24 - m35 + m36 + m37 - m38 + m49 - m50 - m51 + m52
+ n05 = m26 - m28 - m30 + m32 - m40 + m42 + m44 - m46 + m54 - m56 - m58 + m60
+ n06 = m61 - m62 - m63 + m64 + m65 - m66 - m67 + m68 + m69 - m70 - m71 + m72
+ n07 = 2 * n01 + n02 + 2 * n03 + n04 + n05
+ n08 = n01 + n02 + n03 + n06
+
+ # Calculate t, t12, and t3:
+ t = (n07 - sqrt(pow(-n07, 2) - 4 * (n01 + n03 + n04) * n08)) / (2 * n08)
+
+ # t12 can be greatly simplified by defining it with t in it:
+ # If block used to help prevent any div by zero error.
+ t12 = 0
+
+ if a31 == b31:
+ # The line is parallel to the z-axis:
+ if a32 == b32:
+ t12 = ((a11 - a31) + (b11 - a11) * t) / ((a21 - a11) + (a11 - a21 - b11 + b21) * t)
+ # The line is parallel to the y-axis:
+ elif a33 == b33:
+ t12 = ((a11 - a31) + (b11 - a11) * t) / ((a21 - a11) + (a11 - a21 - b11 + b21) * t)
+ # The line is along the y/z-axis but is not parallel to either:
+ else:
+ 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))
+ elif a32 == b32:
+ # The line is parallel to the x-axis:
+ if a33 == b33:
+ t12 = ((a12 - a32) + (b12 - a12) * t) / ((a22 - a12) + (a12 - a22 - b12 + b22) * t)
+ # The line is along the x/z-axis but is not parallel to either:
+ else:
+ 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))
+ # The line is along the x/y-axis but is not parallel to either:
+ else:
+ 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))
+
+ # Likewise, t3 is greatly simplified by defining it in terms of t and t12:
+ # If block used to prevent a div by zero error.
+ t3 = 0
+ if a31 != b31:
+ t3 = (-a11 + a31 + (a11 - b11) * t + (a11 - a21) *
+ t12 + (a21 - a11 + b11 - b21) * t * t12) / (a31 - b31)
+ elif a32 != b32:
+ t3 = (-a12 + a32 + (a12 - b12) * t + (a12 - a22) *
+ t12 + (a22 - a12 + b12 - b22) * t * t12) / (a32 - b32)
+ elif a33 != b33:
+ t3 = (-a13 + a33 + (a13 - b13) * t + (a13 - a23) *
+ t12 + (a23 - a13 + b13 - b23) * t * t12) / (a33 - b33)
+ else:
+ if ENABLE_DEBUG:
+ print("The second edge is a zero-length edge")
+ return None
+
+ # Calculate the point of intersection:
+ x = (1 - t3) * a31 + t3 * b31
+ y = (1 - t3) * a32 + t3 * b32
+ z = (1 - t3) * a33 + t3 * b33
+ int_co = Vector((x, y, z))
+
+ if ENABLE_DEBUG:
+ print(int_co)
+
+ # If the line does not intersect the quad, we return "None":
+ if (t < -1 or t > 1 or t12 < -1 or t12 > 1) and not is_infinite:
+ int_co = None
+
+ elif len(face.verts) == 3:
+ p1, p2, p3 = face.verts[0].co, face.verts[1].co, face.verts[2].co
+ int_co = intersect_line_plane(edge.verts[0].co, edge.verts[1].co, p1, face.normal)
+
+ # Only check if the triangle is not being treated as an infinite plane:
+ # Math based from http://paulbourke.net/geometry/linefacet/
+ if int_co is not None and not is_infinite:
+ pA = p1 - int_co
+ pB = p2 - int_co
+ pC = p3 - int_co
+ # These must be unit vectors, else we risk a domain error:
+ pA.length = 1
+ pB.length = 1
+ pC.length = 1
+ aAB = acos(pA.dot(pB))
+ aBC = acos(pB.dot(pC))
+ aCA = acos(pC.dot(pA))
+ sumA = aAB + aBC + aCA
+
+ # If the point is outside the triangle:
+ if (sumA > (pi + error) and sumA < (pi - error)):
+ int_co = None
+
+ # This is the default case where we either have a planar quad or an n-gon
+ else:
+ int_co = intersect_line_plane(edge.verts[0].co, edge.verts[1].co,
+ face.verts[0].co, face.normal)
+ return int_co
+
+
+# project_point_plane
+# Projects a point onto a plane. Returns a tuple of the projection vector
+# and the projected coordinate
+
+def project_point_plane(pt, plane_co, plane_no):
+ if ENABLE_DEBUG:
+ print("project_point_plane was called")
+ proj_co = intersect_line_plane(pt, pt + plane_no, plane_co, plane_no)
+ proj_ve = proj_co - pt
+ if ENABLE_DEBUG:
+ print("project_point_plane: proj_co is {}\nproj_ve is {}".format(proj_co, proj_ve))
+ return (proj_ve, proj_co)
+
+
+# ------------ CHAMPHER HELPER METHODS -------------
+
+def is_planar_edge(edge, error=0.000002):
+ angle = edge.calc_face_angle()
+ return ((angle < error and angle > -error) or
+ (angle < (180 + error) and angle > (180 - error)))
+
+
+# ------------- EDGE TOOL METHODS -------------------
+
+# Extends an "edge" in two directions:
+# - Requires two vertices to be selected. They do not have to form an edge
+# - Extends "length" in both directions
+
+class Extend(Operator):
+ bl_idname = "mesh.edgetools_extend"
+ bl_label = "Extend"
+ bl_description = "Extend the selected edges of vertex pairs"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ di1: BoolProperty(
+ name="Forwards",
+ description="Extend the edge forwards",
+ default=True
+ )
+ di2: BoolProperty(
+ name="Backwards",
+ description="Extend the edge backwards",
+ default=False
+ )
+ length: FloatProperty(
+ name="Length",
+ description="Length to extend the edge",
+ min=0.0, max=1024.0,
+ default=1.0
+ )
+
+ def draw(self, context):
+ layout = self.layout
+
+ row = layout.row(align=True)
+ row.prop(self, "di1", toggle=True)
+ row.prop(self, "di2", toggle=True)
+
+ layout.prop(self, "length")
+
+ @classmethod
+ def poll(cls, context):
+ ob = context.active_object
+ return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
+
+ def invoke(self, context, event):
+ return self.execute(context)
+
+ def execute(self, context):
+ try:
+ me = context.object.data
+ bm = bmesh.from_edit_mesh(me)
+ bm.normal_update()
+
+ bEdges = bm.edges
+ bVerts = bm.verts
+
+ edges = [e for e in bEdges if e.select]
+ verts = [v for v in bVerts if v.select]
+
+ if not is_selected_enough(self, edges, 0, edges_n=1, verts_n=0, types="Edge"):
+ return {'CANCELLED'}
+
+ if len(edges) > 0:
+ for e in edges:
+ vector = e.verts[0].co - e.verts[1].co
+ vector.length = self.length
+
+ if self.di1:
+ v = bVerts.new()
+ if (vector[0] + vector[1] + vector[2]) < 0:
+ v.co = e.verts[1].co - vector
+ newE = bEdges.new((e.verts[1], v))
+ bEdges.ensure_lookup_table()
+ else:
+ v.co = e.verts[0].co + vector
+ newE = bEdges.new((e.verts[0], v))
+ bEdges.ensure_lookup_table()
+ if self.di2:
+ v = bVerts.new()
+ if (vector[0] + vector[1] + vector[2]) < 0:
+ v.co = e.verts[0].co + vector
+ newE = bEdges.new((e.verts[0], v))
+ bEdges.ensure_lookup_table()
+ else:
+ v.co = e.verts[1].co - vector
+ newE = bEdges.new((e.verts[1], v))
+ bEdges.ensure_lookup_table()
+ else:
+ vector = verts[0].co - verts[1].co
+ vector.length = self.length
+
+ if self.di1:
+ v = bVerts.new()
+ if (vector[0] + vector[1] + vector[2]) < 0:
+ v.co = verts[1].co - vector
+ e = bEdges.new((verts[1], v))
+ bEdges.ensure_lookup_table()
+ else:
+ v.co = verts[0].co + vector
+ e = bEdges.new((verts[0], v))
+ bEdges.ensure_lookup_table()
+ if self.di2:
+ v = bVerts.new()
+ if (vector[0] + vector[1] + vector[2]) < 0:
+ v.co = verts[0].co + vector
+ e = bEdges.new((verts[0], v))
+ bEdges.ensure_lookup_table()
+ else:
+ v.co = verts[1].co - vector
+ e = bEdges.new((verts[1], v))
+ bEdges.ensure_lookup_table()
+
+ bmesh.update_edit_mesh(me)
+
+ except Exception as e:
+ error_handlers(self, "mesh.edgetools_extend", e,
+ reports="Extend Operator failed", func=False)
+ return {'CANCELLED'}
+
+ return {'FINISHED'}
+
+
+# Creates a series of edges between two edges using spline interpolation.
+# This basically just exposes existing functionality in addition to some
+# other common methods: Hermite (c-spline), Bezier, and b-spline. These
+# alternates I coded myself after some extensive research into spline theory
+#
+# @todo Figure out what's wrong with the Blender bezier interpolation
+
+class Spline(Operator):
+ bl_idname = "mesh.edgetools_spline"
+ bl_label = "Spline"
+ bl_description = "Create a spline interplopation between two edges"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ alg: EnumProperty(
+ name="Spline Algorithm",
+ items=[('Blender', "Blender", "Interpolation provided through mathutils.geometry"),
+ ('Hermite', "C-Spline", "C-spline interpolation"),
+ ('Bezier', "Bezier", "Bezier interpolation"),
+ ('B-Spline', "B-Spline", "B-Spline interpolation")],
+ default='Bezier'
+ )
+ segments: IntProperty(
+ name="Segments",
+ description="Number of segments to use in the interpolation",
+ min=2, max=4096,
+ soft_max=1024,
+ default=32
+ )
+ flip1: BoolProperty(
+ name="Flip Edge",
+ description="Flip the direction of the spline on Edge 1",
+ default=False
+ )
+ flip2: BoolProperty(
+ name="Flip Edge",
+ description="Flip the direction of the spline on Edge 2",
+ default=False
+ )
+ ten1: FloatProperty(
+ name="Tension",
+ description="Tension on Edge 1",
+ min=-4096.0, max=4096.0,
+ soft_min=-8.0, soft_max=8.0,
+ default=1.0
+ )
+ ten2: FloatProperty(
+ name="Tension",
+ description="Tension on Edge 2",
+ min=-4096.0, max=4096.0,
+ soft_min=-8.0, soft_max=8.0,
+ default=1.0
+ )
+
+ def draw(self, context):
+ layout = self.layout
+
+ layout.prop(self, "alg")
+ layout.prop(self, "segments")
+
+ layout.label(text="Edge 1:")
+ split = layout.split(factor=0.8, align=True)
+ split.prop(self, "ten1")
+ split.prop(self, "flip1", text="Flip1", toggle=True)
+
+ layout.label(text="Edge 2:")
+ split = layout.split(factor=0.8, align=True)
+ split.prop(self, "ten2")
+ split.prop(self, "flip2", text="Flip2", toggle=True)
+
+ @classmethod
+ def poll(cls, context):
+ ob = context.active_object
+ return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
+
+ def invoke(self, context, event):
+ return self.execute(context)
+
+ def execute(self, context):
+ try:
+ me = context.object.data
+ bm = bmesh.from_edit_mesh(me)
+ bm.normal_update()
+
+ bEdges = bm.edges
+ bVerts = bm.verts
+
+ seg = self.segments
+ edges = [e for e in bEdges if e.select]
+
+ if not is_selected_enough(self, edges, 0, edges_n=2, verts_n=0, types="Edge"):
+ return {'CANCELLED'}
+
+ verts = [edges[v // 2].verts[v % 2] for v in range(4)]
+
+ if self.flip1:
+ v1 = verts[1]
+ p1_co = verts[1].co
+ p1_dir = verts[1].co - verts[0].co
+ else:
+ v1 = verts[0]
+ p1_co = verts[0].co
+ p1_dir = verts[0].co - verts[1].co
+ if self.ten1 < 0:
+ p1_dir = -1 * p1_dir
+ p1_dir.length = -self.ten1
+ else:
+ p1_dir.length = self.ten1
+
+ if self.flip2:
+ v2 = verts[3]
+ p2_co = verts[3].co
+ p2_dir = verts[2].co - verts[3].co
+ else:
+ v2 = verts[2]
+ p2_co = verts[2].co
+ p2_dir = verts[3].co - verts[2].co
+ if self.ten2 < 0:
+ p2_dir = -1 * p2_dir
+ p2_dir.length = -self.ten2
+ else:
+ p2_dir.length = self.ten2
+
+ # Get the interploted coordinates:
+ if self.alg == 'Blender':
+ pieces = interpolate_bezier(
+ p1_co, p1_dir, p2_dir, p2_co, self.segments
+ )
+ elif self.alg == 'Hermite':
+ pieces = interpolate_line_line(
+ p1_co, p1_dir, p2_co, p2_dir, self.segments, 1, 'HERMITE'
+ )
+ elif self.alg == 'Bezier':
+ pieces = interpolate_line_line(
+ p1_co, p1_dir, p2_co, p2_dir, self.segments, 1, 'BEZIER'
+ )
+ elif self.alg == 'B-Spline':
+ pieces = interpolate_line_line(
+ p1_co, p1_dir, p2_co, p2_dir, self.segments, 1, 'BSPLINE'
+ )
+
+ verts = []
+ verts.append(v1)
+ # Add vertices and set the points:
+ for i in range(seg - 1):
+ v = bVerts.new()
+ v.co = pieces[i]
+ bVerts.ensure_lookup_table()
+ verts.append(v)
+ verts.append(v2)
+ # Connect vertices:
+ for i in range(seg):
+ e = bEdges.new((verts[i], verts[i + 1]))
+ bEdges.ensure_lookup_table()
+
+ bmesh.update_edit_mesh(me)
+
+ except Exception as e:
+ error_handlers(self, "mesh.edgetools_spline", e,
+ reports="Spline Operator failed", func=False)
+ return {'CANCELLED'}
+
+ return {'FINISHED'}
+
+
+# Creates edges normal to planes defined between each of two edges and the
+# normal or the plane defined by those two edges.
+# - Select two edges. The must form a plane.
+# - On running the script, eight edges will be created. Delete the
+# extras that you don't need.
+# - The length of those edges is defined by the variable "length"
+#
+# @todo Change method from a cross product to a rotation matrix to make the
+# angle part work.
+# --- todo completed 2/4/2012, but still needs work ---
+# @todo Figure out a way to make +/- predictable
+# - Maybe use angle between edges and vector direction definition?
+# --- TODO COMPLETED ON 2/9/2012 ---
+
+class Ortho(Operator):
+ bl_idname = "mesh.edgetools_ortho"
+ bl_label = "Angle Off Edge"
+ bl_description = "Creates new edges within an angle from vertices of selected edges"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ vert1: BoolProperty(
+ name="Vertice 1",
+ description="Enable edge creation for Vertice 1",
+ default=True
+ )
+ vert2: BoolProperty(
+ name="Vertice 2",
+ description="Enable edge creation for Vertice 2",
+ default=True
+ )
+ vert3: BoolProperty(
+ name="Vertice 3",
+ description="Enable edge creation for Vertice 3",
+ default=True
+ )
+ vert4: BoolProperty(
+ name="Vertice 4",
+ description="Enable edge creation for Vertice 4",
+ default=True
+ )
+ pos: BoolProperty(
+ name="Positive",
+ description="Enable creation of positive direction edges",
+ default=True
+ )
+ neg: BoolProperty(
+ name="Negative",
+ description="Enable creation of negative direction edges",
+ default=True
+ )
+ angle: FloatProperty(
+ name="Angle",
+ description="Define the angle off of the originating edge",
+ min=0.0, max=180.0,
+ default=90.0
+ )
+ length: FloatProperty(
+ name="Length",
+ description="Length of created edges",
+ min=0.0, max=1024.0,
+ default=1.0
+ )
+ # For when only one edge is selected (Possible feature to be testd):
+ plane: EnumProperty(
+ name="Plane",
+ items=[("XY", "X-Y Plane", "Use the X-Y plane as the plane of creation"),
+ ("XZ", "X-Z Plane", "Use the X-Z plane as the plane of creation"),
+ ("YZ", "Y-Z Plane", "Use the Y-Z plane as the plane of creation")],
+ default="XY"
+ )
+
+ def draw(self, context):
+ layout = self.layout
+
+ layout.label(text="Creation:")
+ split = layout.split()
+ col = split.column()
+
+ col.prop(self, "vert1", toggle=True)
+ col.prop(self, "vert2", toggle=True)
+
+ col = split.column()
+ col.prop(self, "vert3", toggle=True)
+ col.prop(self, "vert4", toggle=True)
+
+ layout.label(text="Direction:")
+ row = layout.row(align=False)
+ row.alignment = 'EXPAND'
+ row.prop(self, "pos")
+ row.prop(self, "neg")
+
+ layout.separator()
+
+ col = layout.column(align=True)
+ col.prop(self, "angle")
+ col.prop(self, "length")
+
+ @classmethod
+ def poll(cls, context):
+ ob = context.active_object
+ return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
+
+ def invoke(self, context, event):
+ return self.execute(context)
+
+ def execute(self, context):
+ try:
+ me = context.object.data
+ bm = bmesh.from_edit_mesh(me)
+ bm.normal_update()
+
+ bVerts = bm.verts
+ bEdges = bm.edges
+ edges = [e for e in bEdges if e.select]
+ vectors = []
+
+ if not is_selected_enough(self, edges, 0, edges_n=2, verts_n=0, types="Edge"):
+ return {'CANCELLED'}
+
+ verts = [edges[0].verts[0],
+ edges[0].verts[1],
+ edges[1].verts[0],
+ edges[1].verts[1]]
+
+ cos = intersect_line_line(verts[0].co, verts[1].co, verts[2].co, verts[3].co)
+
+ # If the two edges are parallel:
+ if cos is None:
+ self.report({'WARNING'},
+ "Selected lines are parallel: results may be unpredictable")
+ vectors.append(verts[0].co - verts[1].co)
+ vectors.append(verts[0].co - verts[2].co)
+ vectors.append(vectors[0].cross(vectors[1]))
+ vectors.append(vectors[2].cross(vectors[0]))
+ vectors.append(-vectors[3])
+ else:
+ # Warn the user if they have not chosen two planar edges:
+ if not is_same_co(cos[0], cos[1]):
+ self.report({'WARNING'},
+ "Selected lines are not planar: results may be unpredictable")
+
+ # This makes the +/- behavior predictable:
+ if (verts[0].co - cos[0]).length < (verts[1].co - cos[0]).length:
+ verts[0], verts[1] = verts[1], verts[0]
+ if (verts[2].co - cos[0]).length < (verts[3].co - cos[0]).length:
+ verts[2], verts[3] = verts[3], verts[2]
+
+ vectors.append(verts[0].co - verts[1].co)
+ vectors.append(verts[2].co - verts[3].co)
+
+ # Normal of the plane formed by vector1 and vector2:
+ vectors.append(vectors[0].cross(vectors[1]))
+
+ # Possible directions:
+ vectors.append(vectors[2].cross(vectors[0]))
+ vectors.append(vectors[1].cross(vectors[2]))
+
+ # Set the length:
+ vectors[3].length = self.length
+ vectors[4].length = self.length
+
+ # Perform any additional rotations:
+ matrix = Matrix.Rotation(radians(90 + self.angle), 3, vectors[2])
+ vectors.append(matrix @ -vectors[3]) # vectors[5]
+ matrix = Matrix.Rotation(radians(90 - self.angle), 3, vectors[2])
+ vectors.append(matrix @ vectors[4]) # vectors[6]
+ vectors.append(matrix @ vectors[3]) # vectors[7]
+ matrix = Matrix.Rotation(radians(90 + self.angle), 3, vectors[2])
+ vectors.append(matrix @ -vectors[4]) # vectors[8]
+
+ # Perform extrusions and displacements:
+ # There will be a total of 8 extrusions. One for each vert of each edge.
+ # It looks like an extrusion will add the new vert to the end of the verts
+ # list and leave the rest in the same location.
+ # -- EDIT --
+ # It looks like I might be able to do this within "bpy.data" with the ".add" function
+
+ for v in range(len(verts)):
+ vert = verts[v]
+ if ((v == 0 and self.vert1) or (v == 1 and self.vert2) or
+ (v == 2 and self.vert3) or (v == 3 and self.vert4)):
+
+ if self.pos:
+ new = bVerts.new()
+ new.co = vert.co - vectors[5 + (v // 2) + ((v % 2) * 2)]
+ bVerts.ensure_lookup_table()
+ bEdges.new((vert, new))
+ bEdges.ensure_lookup_table()
+ if self.neg:
+ new = bVerts.new()
+ new.co = vert.co + vectors[5 + (v // 2) + ((v % 2) * 2)]
+ bVerts.ensure_lookup_table()
+ bEdges.new((vert, new))
+ bEdges.ensure_lookup_table()
+
+ bmesh.update_edit_mesh(me)
+ except Exception as e:
+ error_handlers(self, "mesh.edgetools_ortho", e,
+ reports="Angle Off Edge Operator failed", func=False)
+ return {'CANCELLED'}
+
+ return {'FINISHED'}
+
+
+# Usage:
+# Select an edge and a point or an edge and specify the radius (default is 1 BU)
+# You can select two edges but it might be unpredictable which edge it revolves
+# around so you might have to play with the switch
+
+class Shaft(Operator):
+ bl_idname = "mesh.edgetools_shaft"
+ bl_label = "Shaft"
+ bl_description = "Create a shaft mesh around an axis"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ # Selection defaults:
+ shaftType = 0
+
+ # For tracking if the user has changed selection:
+ last_edge: IntProperty(
+ name="Last Edge",
+ description="Tracks if user has changed selected edges",
+ min=0, max=1,
+ default=0
+ )
+ last_flip = False
+
+ edge: IntProperty(
+ name="Edge",
+ description="Edge to shaft around",
+ min=0, max=1,
+ default=0
+ )
+ flip: BoolProperty(
+ name="Flip Second Edge",
+ description="Flip the perceived direction of the second edge",
+ default=False
+ )
+ radius: FloatProperty(
+ name="Radius",
+ description="Shaft Radius",
+ min=0.0, max=1024.0,
+ default=1.0
+ )
+ start: FloatProperty(
+ name="Starting Angle",
+ description="Angle to start the shaft at",
+ min=-360.0, max=360.0,
+ default=0.0
+ )
+ finish: FloatProperty(
+ name="Ending Angle",
+ description="Angle to end the shaft at",
+ min=-360.0, max=360.0,
+ default=360.0
+ )
+ segments: IntProperty(
+ name="Shaft Segments",
+ description="Number of segments to use in the shaft",
+ min=1, max=4096,
+ soft_max=512,
+ default=32
+ )
+
+ def draw(self, context):
+ layout = self.layout
+
+ if self.shaftType == 0:
+ layout.prop(self, "edge")
+ layout.prop(self, "flip")
+ elif self.shaftType == 3:
+ layout.prop(self, "radius")
+
+ layout.prop(self, "segments")
+ layout.prop(self, "start")
+ layout.prop(self, "finish")
+
+ @classmethod
+ def poll(cls, context):
+ ob = context.active_object
+ return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
+
+ def invoke(self, context, event):
+ # Make sure these get reset each time we run:
+ self.last_edge = 0
+ self.edge = 0
+
+ return self.execute(context)
+
+ def execute(self, context):
+ try:
+ me = context.object.data
+ bm = bmesh.from_edit_mesh(me)
+ bm.normal_update()
+
+ bFaces = bm.faces
+ bEdges = bm.edges
+ bVerts = bm.verts
+
+ active = None
+ edges, verts = [], []
+
+ # Pre-caclulated values:
+ rotRange = [radians(self.start), radians(self.finish)]
+ rads = radians((self.finish - self.start) / self.segments)
+
+ numV = self.segments + 1
+ numE = self.segments
+
+ edges = [e for e in bEdges if e.select]
+
+ # Robustness check: there should at least be one edge selected
+ if not is_selected_enough(self, edges, 0, edges_n=1, verts_n=0, types="Edge"):
+ return {'CANCELLED'}
+
+ # If two edges are selected:
+ if len(edges) == 2:
+ # default:
+ edge = [0, 1]
+ vert = [0, 1]
+
+ # By default, we want to shaft around the last selected edge (it
+ # will be the active edge). We know we are using the default if
+ # the user has not changed which edge is being shafted around (as
+ # is tracked by self.last_edge). When they are not the same, then
+ # the user has changed selection.
+ # We then need to make sure that the active object really is an edge
+ # (robustness check)
+ # Finally, if the active edge is not the initial one, we flip them
+ # and have the GUI reflect that
+ if self.last_edge == self.edge:
+ if isinstance(bm.select_history.active, bmesh.types.BMEdge):
+ if bm.select_history.active != edges[edge[0]]:
+ self.last_edge, self.edge = edge[1], edge[1]
+ edge = [edge[1], edge[0]]
+ else:
+ flip_edit_mode()
+ self.report({'WARNING'},
+ "Active geometry is not an edge. Operation Cancelled")
+ return {'CANCELLED'}
+ elif self.edge == 1:
+ edge = [1, 0]
+
+ verts.append(edges[edge[0]].verts[0])
+ verts.append(edges[edge[0]].verts[1])
+
+ if self.flip:
+ verts = [1, 0]
+
+ verts.append(edges[edge[1]].verts[vert[0]])
+ verts.append(edges[edge[1]].verts[vert[1]])
+
+ self.shaftType = 0
+ # If there is more than one edge selected:
+ # There are some issues with it ATM, so don't expose is it to normal users
+ # @todo Fix edge connection ordering issue
+ elif ENABLE_DEBUG and len(edges) > 2:
+ if isinstance(bm.select_history.active, bmesh.types.BMEdge):
+ active = bm.select_history.active
+ edges.remove(active)
+ # Get all the verts:
+ # edges = order_joined_edges(edges[0])
+ verts = []
+ for e in edges:
+ if verts.count(e.verts[0]) == 0:
+ verts.append(e.verts[0])
+ if verts.count(e.verts[1]) == 0:
+ verts.append(e.verts[1])
+ else:
+ flip_edit_mode()
+ self.report({'WARNING'},
+ "Active geometry is not an edge. Operation Cancelled")
+ return {'CANCELLED'}
+ self.shaftType = 1
+ else:
+ verts.append(edges[0].verts[0])
+ verts.append(edges[0].verts[1])
+
+ for v in bVerts:
+ if v.select and verts.count(v) == 0:
+ verts.append(v)
+ v.select = False
+ if len(verts) == 2:
+ self.shaftType = 3
+ else:
+ self.shaftType = 2
+
+ # The vector denoting the axis of rotation:
+ if self.shaftType == 1:
+ axis = active.verts[1].co - active.verts[0].co
+ else:
+ axis = verts[1].co - verts[0].co
+
+ # We will need a series of rotation matrices. We could use one which
+ # would be faster but also might cause propagation of error
+ # matrices = []
+ # for i in range(numV):
+ # matrices.append(Matrix.Rotation((rads * i) + rotRange[0], 3, axis))
+ matrices = [Matrix.Rotation((rads * i) + rotRange[0], 3, axis) for i in range(numV)]
+
+ # New vertice coordinates:
+ verts_out = []
+
+ # If two edges were selected:
+ # - If the lines are not parallel, then it will create a cone-like shaft
+ if self.shaftType == 0:
+ for i in range(len(verts) - 2):
+ init_vec = distance_point_line(verts[i + 2].co, verts[0].co, verts[1].co)
+ co = init_vec + verts[i + 2].co
+ # These will be rotated about the origin so will need to be shifted:
+ for j in range(numV):
+ verts_out.append(co - (matrices[j] @ init_vec))
+ elif self.shaftType == 1:
+ for i in verts:
+ init_vec = distance_point_line(i.co, active.verts[0].co, active.verts[1].co)
+ co = init_vec + i.co
+ # These will be rotated about the origin so will need to be shifted:
+ for j in range(numV):
+ verts_out.append(co - (matrices[j] @ init_vec))
+ # Else if a line and a point was selected:
+ elif self.shaftType == 2:
+ init_vec = distance_point_line(verts[2].co, verts[0].co, verts[1].co)
+ # These will be rotated about the origin so will need to be shifted:
+ verts_out = [
+ (verts[i].co - (matrices[j] @ init_vec)) for i in range(2) for j in range(numV)
+ ]
+ else:
+ # Else the above are not possible, so we will just use the edge:
+ # - The vector defined by the edge is the normal of the plane for the shaft
+ # - The shaft will have radius "radius"
+ if is_axial(verts[0].co, verts[1].co) is None:
+ proj = (verts[1].co - verts[0].co)
+ proj[2] = 0
+ norm = proj.cross(verts[1].co - verts[0].co)
+ vec = norm.cross(verts[1].co - verts[0].co)
+ vec.length = self.radius
+ elif is_axial(verts[0].co, verts[1].co) == 'Z':
+ vec = verts[0].co + Vector((0, 0, self.radius))
+ else:
+ vec = verts[0].co + Vector((0, self.radius, 0))
+ init_vec = distance_point_line(vec, verts[0].co, verts[1].co)
+ # These will be rotated about the origin so will need to be shifted:
+ verts_out = [
+ (verts[i].co - (matrices[j] @ init_vec)) for i in range(2) for j in range(numV)
+ ]
+
+ # We should have the coordinates for a bunch of new verts
+ # Now add the verts and build the edges and then the faces
+
+ newVerts = []
+
+ if self.shaftType == 1:
+ # Vertices:
+ for i in range(numV * len(verts)):
+ new = bVerts.new()
+ new.co = verts_out[i]
+ bVerts.ensure_lookup_table()
+ new.select = True
+ newVerts.append(new)
+ # Edges:
+ for i in range(numE):
+ for j in range(len(verts)):
+ e = bEdges.new((newVerts[i + (numV * j)], newVerts[i + (numV * j) + 1]))
+ bEdges.ensure_lookup_table()
+ e.select = True
+ for i in range(numV):
+ for j in range(len(verts) - 1):
+ e = bEdges.new((newVerts[i + (numV * j)], newVerts[i + (numV * (j + 1))]))
+ bEdges.ensure_lookup_table()
+ e.select = True
+
+ # Faces: There is a problem with this right now
+ """
+ for i in range(len(edges)):
+ for j in range(numE):
+ f = bFaces.new((newVerts[i], newVerts[i + 1],
+ newVerts[i + (numV * j) + 1], newVerts[i + (numV * j)]))
+ f.normal_update()
+ """
+ else:
+ # Vertices:
+ for i in range(numV * 2):
+ new = bVerts.new()
+ new.co = verts_out[i]
+ new.select = True
+ bVerts.ensure_lookup_table()
+ newVerts.append(new)
+ # Edges:
+ for i in range(numE):
+ e = bEdges.new((newVerts[i], newVerts[i + 1]))
+ e.select = True
+ bEdges.ensure_lookup_table()
+ e = bEdges.new((newVerts[i + numV], newVerts[i + numV + 1]))
+ e.select = True
+ bEdges.ensure_lookup_table()
+ for i in range(numV):
+ e = bEdges.new((newVerts[i], newVerts[i + numV]))
+ e.select = True
+ bEdges.ensure_lookup_table()
+ # Faces:
+ for i in range(numE):
+ f = bFaces.new((newVerts[i], newVerts[i + 1],
+ newVerts[i + numV + 1], newVerts[i + numV]))
+ bFaces.ensure_lookup_table()
+ f.normal_update()
+
+ bmesh.update_edit_mesh(me)
+
+ except Exception as e:
+ error_handlers(self, "mesh.edgetools_shaft", e,
+ reports="Shaft Operator failed", func=False)
+ return {'CANCELLED'}
+
+ return {'FINISHED'}
+
+
+# "Slices" edges crossing a plane defined by a face
+
+class Slice(Operator):
+ bl_idname = "mesh.edgetools_slice"
+ bl_label = "Slice"
+ bl_description = "Cut edges at the plane defined by a selected face"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ make_copy: BoolProperty(
+ name="Make Copy",
+ description="Make new vertices at intersection points instead of splitting the edge",
+ default=False
+ )
+ rip: BoolProperty(
+ name="Rip",
+ description="Split into two edges that DO NOT share an intersection vertex",
+ default=True
+ )
+ pos: BoolProperty(
+ name="Positive",
+ description="Remove the portion on the side of the face normal",
+ default=False
+ )
+ neg: BoolProperty(
+ name="Negative",
+ description="Remove the portion on the side opposite of the face normal",
+ default=False
+ )
+
+ def draw(self, context):
+ layout = self.layout
+
+ layout.prop(self, "make_copy")
+ if not self.make_copy:
+ layout.prop(self, "rip")
+ layout.label(text="Remove Side:")
+ layout.prop(self, "pos")
+ layout.prop(self, "neg")
+
+ @classmethod
+ def poll(cls, context):
+ ob = context.active_object
+ return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
+
+ def invoke(self, context, event):
+ return self.execute(context)
+
+ def execute(self, context):
+ try:
+ me = context.object.data
+ bm = bmesh.from_edit_mesh(me)
+ bm.normal_update()
+
+ bVerts = bm.verts
+ bEdges = bm.edges
+ bFaces = bm.faces
+
+ face, normal = None, None
+
+ # Find the selected face. This will provide the plane to project onto:
+ # - First check to use the active face. Allows users to just
+ # select a bunch of faces with the last being the cutting plane
+ # - If that fails, then use the first found selected face in the BMesh face list
+ if isinstance(bm.select_history.active, bmesh.types.BMFace):
+ face = bm.select_history.active
+ normal = bm.select_history.active.normal
+ bm.select_history.active.select = False
+ else:
+ for f in bFaces:
+ if f.select:
+ face = f
+ normal = f.normal
+ f.select = False
+ break
+
+ # If we don't find a selected face exit:
+ if face is None:
+ flip_edit_mode()
+ self.report({'WARNING'},
+ "Please select a face as the cutting plane. Operation Cancelled")
+ return {'CANCELLED'}
+
+ # Warn the user if they are using an n-gon might lead to some odd results
+ elif len(face.verts) > 4 and not is_face_planar(face):
+ self.report({'WARNING'},
+ "Selected face is an N-gon. Results may be unpredictable")
+
+ if ENABLE_DEBUG:
+ dbg = 0
+ print("Number of Edges: ", len(bEdges))
+
+ for e in bEdges:
+ if ENABLE_DEBUG:
+ print("Looping through Edges - ", dbg)
+ dbg = dbg + 1
+
+ # Get the end verts on the edge:
+ v1 = e.verts[0]
+ v2 = e.verts[1]
+
+ # Make sure that verts are not a part of the cutting plane:
+ if e.select and (v1 not in face.verts and v2 not in face.verts):
+ if len(face.verts) < 5: # Not an n-gon
+ intersection = intersect_line_face(e, face, True)
+ else:
+ intersection = intersect_line_plane(v1.co, v2.co, face.verts[0].co, normal)
+
+ if ENABLE_DEBUG:
+ print("Intersection: ", intersection)
+
+ # If an intersection exists find the distance of each of the end
+ # points from the plane, with "positive" being in the direction
+ # of the cutting plane's normal. If the points are on opposite
+ # side of the plane, then it intersects and we need to cut it
+ if intersection is not None:
+ bVerts.ensure_lookup_table()
+ bEdges.ensure_lookup_table()
+ bFaces.ensure_lookup_table()
+
+ d1 = distance_point_to_plane(v1.co, face.verts[0].co, normal)
+ d2 = distance_point_to_plane(v2.co, face.verts[0].co, normal)
+ # If they have different signs, then the edge crosses the cutting plane:
+ if abs(d1 + d2) < abs(d1 - d2):
+ # Make the first vertex the positive one:
+ if d1 < d2:
+ v2, v1 = v1, v2
+
+ if self.make_copy:
+ new = bVerts.new()
+ new.co = intersection
+ new.select = True
+ bVerts.ensure_lookup_table()
+ elif self.rip:
+ if ENABLE_DEBUG:
+ print("Branch rip engaged")
+ newV1 = bVerts.new()
+ newV1.co = intersection
+ bVerts.ensure_lookup_table()
+ if ENABLE_DEBUG:
+ print("newV1 created", end='; ')
+
+ newV2 = bVerts.new()
+ newV2.co = intersection
+ bVerts.ensure_lookup_table()
+
+ if ENABLE_DEBUG:
+ print("newV2 created", end='; ')
+
+ newE1 = bEdges.new((v1, newV1))
+ newE2 = bEdges.new((v2, newV2))
+ bEdges.ensure_lookup_table()
+
+ if ENABLE_DEBUG:
+ print("new edges created", end='; ')
+
+ if e.is_valid:
+ bEdges.remove(e)
+
+ bEdges.ensure_lookup_table()
+
+ if ENABLE_DEBUG:
+ print("Old edge removed.\nWe're done with this edge")
+ else:
+ new = list(bmesh.utils.edge_split(e, v1, 0.5))
+ bEdges.ensure_lookup_table()
+ new[1].co = intersection
+ e.select = False
+ new[0].select = False
+ if self.pos:
+ bEdges.remove(new[0])
+ if self.neg:
+ bEdges.remove(e)
+ bEdges.ensure_lookup_table()
+
+ if ENABLE_DEBUG:
+ print("The Edge Loop has exited. Now to update the bmesh")
+ dbg = 0
+
+ bmesh.update_edit_mesh(me)
+
+ except Exception as e:
+ error_handlers(self, "mesh.edgetools_slice", e,
+ reports="Slice Operator failed", func=False)
+ return {'CANCELLED'}
+
+ return {'FINISHED'}
+
+
+# This projects the selected edges onto the selected plane
+# and/or both points on the selected edge
+
+class Project(Operator):
+ bl_idname = "mesh.edgetools_project"
+ bl_label = "Project"
+ bl_description = ("Projects the selected Vertices/Edges onto a selected plane\n"
+ "(Active is projected onto the rest)")
+ bl_options = {'REGISTER', 'UNDO'}
+
+ make_copy: BoolProperty(
+ name="Make Copy",
+ description="Make duplicates of the vertices instead of altering them",
+ default=False
+ )
+
+ def draw(self, context):
+ layout = self.layout
+ layout.prop(self, "make_copy")
+
+ @classmethod
+ def poll(cls, context):
+ ob = context.active_object
+ return (ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
+
+ def invoke(self, context, event):
+ return self.execute(context)
+
+ def execute(self, context):
+ try:
+ me = context.object.data
+ bm = bmesh.from_edit_mesh(me)
+ bm.normal_update()
+
+ bFaces = bm.faces
+ bVerts = bm.verts
+
+ fVerts = []
+
+ # Find the selected face. This will provide the plane to project onto:
+ # @todo Check first for an active face
+ for f in bFaces:
+ if f.select:
+ for v in f.verts:
+ fVerts.append(v)
+ normal = f.normal
+ f.select = False
+ break
+
+ for v in bVerts:
+ if v.select:
+ if v in fVerts:
+ v.select = False
+ continue
+ d = distance_point_to_plane(v.co, fVerts[0].co, normal)
+ if self.make_copy:
+ temp = v
+ v = bVerts.new()
+ v.co = temp.co
+ bVerts.ensure_lookup_table()
+ vector = normal
+ vector.length = abs(d)
+ v.co = v.co - (vector * sign(d))
+ v.select = False
+
+ bmesh.update_edit_mesh(me)
+
+ except Exception as e:
+ error_handlers(self, "mesh.edgetools_project", e,
+ reports="Project Operator failed", func=False)
+
+ return {'CANCELLED'}
+
+ return {'FINISHED'}
+
+
+# Project_End is for projecting/extending an edge to meet a plane
+# This is used be selecting a face to define the plane then all the edges
+# Then move the vertices in the edge that is closest to the
+# plane to the coordinates of the intersection of the edge and the plane
+
+class Project_End(Operator):
+ bl_idname = "mesh.edgetools_project_end"
+ bl_label = "Project (End Point)"
+ bl_description = ("Projects the vertices of the selected\n"
+ "edges closest to a plane onto that plane")
+ bl_options = {'REGISTER', 'UNDO'}
+
+ make_copy: BoolProperty(
+ name="Make Copy",
+ description="Make a duplicate of the vertice instead of moving it",
+ default=False
+ )
+ keep_length: BoolProperty(
+ name="Keep Edge Length",
+ description="Maintain edge lengths",
+ default=False
+ )
+ use_force: BoolProperty(
+ name="Use opposite vertices",
+ description="Force the usage of the vertices at the other end of the edge",
+ default=False
+ )
+ use_normal: BoolProperty(
+ name="Project along normal",
+ description="Use the plane's normal as the projection direction",
+ default=False
+ )
+
+ def draw(self, context):
+ layout = self.layout
+
+ if not self.keep_length:
+ layout.prop(self, "use_normal")
+ layout.prop(self, "make_copy")
+ layout.prop(self, "use_force")
+
+ @classmethod
+ def poll(cls, context):
+ ob = context.active_object
+ return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
+
+ def invoke(self, context, event):
+ return self.execute(context)
+
+ def execute(self, context):
+ try:
+ me = context.object.data
+ bm = bmesh.from_edit_mesh(me)
+ bm.normal_update()
+
+ bFaces = bm.faces
+ bEdges = bm.edges
+ bVerts = bm.verts
+
+ fVerts = []
+
+ # Find the selected face. This will provide the plane to project onto:
+ for f in bFaces:
+ if f.select:
+ for v in f.verts:
+ fVerts.append(v)
+ normal = f.normal
+ f.select = False
+ break
+
+ for e in bEdges:
+ if e.select:
+ v1 = e.verts[0]
+ v2 = e.verts[1]
+ if v1 in fVerts or v2 in fVerts:
+ e.select = False
+ continue
+ intersection = intersect_line_plane(v1.co, v2.co, fVerts[0].co, normal)
+ if intersection is not None:
+ # Use abs because we don't care what side of plane we're on:
+ d1 = distance_point_to_plane(v1.co, fVerts[0].co, normal)
+ d2 = distance_point_to_plane(v2.co, fVerts[0].co, normal)
+ # If d1 is closer than we use v1 as our vertice:
+ # "xor" with 'use_force':
+ if (abs(d1) < abs(d2)) is not self.use_force:
+ if self.make_copy:
+ v1 = bVerts.new()
+ v1.co = e.verts[0].co
+ bVerts.ensure_lookup_table()
+ bEdges.ensure_lookup_table()
+ if self.keep_length:
+ v1.co = intersection
+ elif self.use_normal:
+ vector = normal
+ vector.length = abs(d1)
+ v1.co = v1.co - (vector * sign(d1))
+ else:
+ v1.co = intersection
+ else:
+ if self.make_copy:
+ v2 = bVerts.new()
+ v2.co = e.verts[1].co
+ bVerts.ensure_lookup_table()
+ bEdges.ensure_lookup_table()
+ if self.keep_length:
+ v2.co = intersection
+ elif self.use_normal:
+ vector = normal
+ vector.length = abs(d2)
+ v2.co = v2.co - (vector * sign(d2))
+ else:
+ v2.co = intersection
+ e.select = False
+
+ bmesh.update_edit_mesh(me)
+
+ except Exception as e:
+ error_handlers(self, "mesh.edgetools_project_end", e,
+ reports="Project (End Point) Operator failed", func=False)
+ return {'CANCELLED'}
+
+ return {'FINISHED'}
+
+
+class VIEW3D_MT_edit_mesh_edgetools(Menu):
+ bl_label = "Edge Tools"
+ bl_description = "Various tools for manipulating edges"
+
+ def draw(self, context):
+ layout = self.layout
+
+ layout.operator("mesh.edgetools_extend")
+ layout.operator("mesh.edgetools_spline")
+ layout.operator("mesh.edgetools_ortho")
+ layout.operator("mesh.edgetools_shaft")
+ layout.operator("mesh.edgetools_slice")
+ layout.separator()
+
+ layout.operator("mesh.edgetools_project")
+ layout.operator("mesh.edgetools_project_end")
+
+def menu_func(self, context):
+ self.layout.menu("VIEW3D_MT_edit_mesh_edgetools")
+
+# define classes for registration
+classes = (
+ VIEW3D_MT_edit_mesh_edgetools,
+ Extend,
+ Spline,
+ Ortho,
+ Shaft,
+ Slice,
+ Project,
+ Project_End,
+ )
+
+
+# registering and menu integration
+def register():
+ for cls in classes:
+ bpy.utils.register_class(cls)
+ bpy.types.VIEW3D_MT_edit_mesh_context_menu.prepend(menu_func)
+
+# unregistering and removing menus
+def unregister():
+ for cls in classes:
+ bpy.utils.unregister_class(cls)
+ bpy.types.VIEW3D_MT_edit_mesh_context_menu.remove(menu_func)
+
+if __name__ == "__main__":
+ register()