AnimAll: update translation
[blender-addons.git] / mesh_tools / mesh_edgetools.py
blob920e9a2786efc4fcb5a981bec0a41c548540b705
1 # SPDX-FileCopyrightText: 2012 Paul Marshall
3 # SPDX-License-Identifier: GPL-2.0-or-later
5 # The Blender Edgetools is to bring CAD tools to Blender.
7 bl_info = {
8 "name": "EdgeTools",
9 "author": "Paul Marshall",
10 "version": (0, 9, 2),
11 "blender": (2, 80, 0),
12 "location": "View3D > Toolbar and View3D > Specials (W-key)",
13 "warning": "",
14 "description": "CAD style edge manipulation tools",
15 "doc_url": "https://wiki.blender.org/index.php/Extensions:2.6/Py/"
16 "Scripts/Modeling/EdgeTools",
17 "category": "Mesh",
21 import bpy
22 import bmesh
23 from bpy.types import (
24 Operator,
25 Menu,
27 from math import acos, pi, radians, sqrt
28 from mathutils import Matrix, Vector
29 from mathutils.geometry import (
30 distance_point_to_plane,
31 interpolate_bezier,
32 intersect_point_line,
33 intersect_line_line,
34 intersect_line_plane,
36 from bpy.props import (
37 BoolProperty,
38 IntProperty,
39 FloatProperty,
40 EnumProperty,
43 """
44 Blender EdgeTools
45 This is a toolkit for edge manipulation based on mesh manipulation
46 abilities of several CAD/CAE packages, notably CATIA's Geometric Workbench
47 from which most of these tools have a functional basis.
49 The GUI and Blender add-on structure shamelessly coded in imitation of the
50 LoopTools addon.
52 Examples:
53 - "Ortho" inspired from CATIA's line creation tool which creates a line of a
54 user specified length at a user specified angle to a curve at a chosen
55 point. The user then selects the plane the line is to be created in.
56 - "Shaft" is inspired from CATIA's tool of the same name. However, instead
57 of a curve around an axis, this will instead shaft a line, a point, or
58 a fixed radius about the selected axis.
59 - "Slice" is from CATIA's ability to split a curve on a plane. When
60 completed this be a Python equivalent with all the same basic
61 functionality, though it will sadly be a little clumsier to use due
62 to Blender's selection limitations.
64 Notes:
65 - Fillet operator and related functions removed as they didn't work
66 - Buggy parts have been hidden behind ENABLE_DEBUG global (set it to True)
67 Example: Shaft with more than two edges selected
69 Paul "BrikBot" Marshall
70 Created: January 28, 2012
71 Last Modified: October 6, 2012
73 Coded in IDLE, tested in Blender 2.6.
74 Search for "@todo" to quickly find sections that need work
76 Note: lijenstina - modified this script in preparation for merging
77 fixed the needless jumping to object mode for bmesh creation
78 causing the crash with the Slice > Rip operator
79 Removed the test operator since version 0.9.2
80 added general error handling
81 """
83 # Enable debug
84 # Set to True to have the debug prints available
85 ENABLE_DEBUG = False
88 # Quick an dirty method for getting the sign of a number:
89 def sign(number):
90 return (number > 0) - (number < 0)
93 # is_parallel
94 # Checks to see if two lines are parallel
96 def is_parallel(v1, v2, v3, v4):
97 result = intersect_line_line(v1, v2, v3, v4)
98 return result is None
101 # Handle error notifications
102 def error_handlers(self, op_name, error, reports="ERROR", func=False):
103 if self and reports:
104 self.report({'WARNING'}, reports + " (See Console for more info)")
106 is_func = "Function" if func else "Operator"
107 print("\n[Mesh EdgeTools]\n{}: {}\nError: {}\n".format(is_func, op_name, error))
110 def flip_edit_mode():
111 bpy.ops.object.editmode_toggle()
112 bpy.ops.object.editmode_toggle()
115 # check the appropriate selection condition
116 # to prevent crashes with the index out of range errors
117 # pass the bEdges and bVerts based selection tables here
118 # types: Edge, Vertex, All
119 def is_selected_enough(self, bEdges, bVerts, edges_n=1, verts_n=0, types="Edge"):
120 check = False
121 try:
122 if bEdges and types == "Edge":
123 check = (len(bEdges) >= edges_n)
124 elif bVerts and types == "Vertex":
125 check = (len(bVerts) >= verts_n)
126 elif bEdges and bVerts and types == "All":
127 check = (len(bEdges) >= edges_n and len(bVerts) >= verts_n)
129 if check is False:
130 strings = "%s Vertices and / or " % verts_n if verts_n != 0 else ""
131 self.report({'WARNING'},
132 "Needs at least " + strings + "%s Edge(s) selected. "
133 "Operation Cancelled" % edges_n)
134 flip_edit_mode()
136 return check
138 except Exception as e:
139 error_handlers(self, "is_selected_enough", e,
140 "No appropriate selection. Operation Cancelled", func=True)
141 return False
143 return False
146 # is_axial
147 # This is for the special case where the edge is parallel to an axis.
148 # The projection onto the XY plane will fail so it will have to be handled differently
150 def is_axial(v1, v2, error=0.000002):
151 vector = v2 - v1
152 # Don't need to store, but is easier to read:
153 vec0 = vector[0] > -error and vector[0] < error
154 vec1 = vector[1] > -error and vector[1] < error
155 vec2 = vector[2] > -error and vector[2] < error
156 if (vec0 or vec1) and vec2:
157 return 'Z'
158 elif vec0 and vec1:
159 return 'Y'
160 return None
163 # is_same_co
164 # For some reason "Vector = Vector" does not seem to look at the actual coordinates
166 def is_same_co(v1, v2):
167 if len(v1) != len(v2):
168 return False
169 else:
170 for co1, co2 in zip(v1, v2):
171 if co1 != co2:
172 return False
173 return True
176 def is_face_planar(face, error=0.0005):
177 for v in face.verts:
178 d = distance_point_to_plane(v.co, face.verts[0].co, face.normal)
179 if ENABLE_DEBUG:
180 print("Distance: " + str(d))
181 if d < -error or d > error:
182 return False
183 return True
186 # other_joined_edges
187 # Starts with an edge. Then scans for linked, selected edges and builds a
188 # list with them in "order", starting at one end and moving towards the other
190 def order_joined_edges(edge, edges=[], direction=1):
191 if len(edges) == 0:
192 edges.append(edge)
193 edges[0] = edge
195 if ENABLE_DEBUG:
196 print(edge, end=", ")
197 print(edges, end=", ")
198 print(direction, end="; ")
200 # Robustness check: direction cannot be zero
201 if direction == 0:
202 direction = 1
204 newList = []
205 for e in edge.verts[0].link_edges:
206 if e.select and edges.count(e) == 0:
207 if direction > 0:
208 edges.insert(0, e)
209 newList.extend(order_joined_edges(e, edges, direction + 1))
210 newList.extend(edges)
211 else:
212 edges.append(e)
213 newList.extend(edges)
214 newList.extend(order_joined_edges(e, edges, direction - 1))
216 # This will only matter at the first level:
217 direction = direction * -1
219 for e in edge.verts[1].link_edges:
220 if e.select and edges.count(e) == 0:
221 if direction > 0:
222 edges.insert(0, e)
223 newList.extend(order_joined_edges(e, edges, direction + 2))
224 newList.extend(edges)
225 else:
226 edges.append(e)
227 newList.extend(edges)
228 newList.extend(order_joined_edges(e, edges, direction))
230 if ENABLE_DEBUG:
231 print(newList, end=", ")
232 print(direction)
234 return newList
237 # --------------- GEOMETRY CALCULATION METHODS --------------
239 # distance_point_line
240 # I don't know why the mathutils.geometry API does not already have this, but
241 # it is trivial to code using the structures already in place. Instead of
242 # returning a float, I also want to know the direction vector defining the
243 # distance. Distance can be found with "Vector.length"
245 def distance_point_line(pt, line_p1, line_p2):
246 int_co = intersect_point_line(pt, line_p1, line_p2)
247 distance_vector = int_co[0] - pt
248 return distance_vector
251 # interpolate_line_line
252 # This is an experiment into a cubic Hermite spline (c-spline) for connecting
253 # two edges with edges that obey the general equation.
254 # This will return a set of point coordinates (Vectors)
256 # A good, easy to read background on the mathematics can be found at:
257 # http://cubic.org/docs/hermite.htm
259 # Right now this is . . . less than functional :P
260 # @todo
261 # - C-Spline and Bezier curves do not end on p2_co as they are supposed to.
262 # - B-Spline just fails. Epically.
263 # - Add more methods as I come across them. Who said flexibility was bad?
265 def interpolate_line_line(p1_co, p1_dir, p2_co, p2_dir, segments, tension=1,
266 typ='BEZIER', include_ends=False):
267 pieces = []
268 fraction = 1 / segments
270 # Form: p1, tangent 1, p2, tangent 2
271 if typ == 'HERMITE':
272 poly = [[2, -3, 0, 1], [1, -2, 1, 0],
273 [-2, 3, 0, 0], [1, -1, 0, 0]]
274 elif typ == 'BEZIER':
275 poly = [[-1, 3, -3, 1], [3, -6, 3, 0],
276 [1, 0, 0, 0], [-3, 3, 0, 0]]
277 p1_dir = p1_dir + p1_co
278 p2_dir = -p2_dir + p2_co
279 elif typ == 'BSPLINE':
280 # Supposed poly matrix for a cubic b-spline:
281 # poly = [[-1, 3, -3, 1], [3, -6, 3, 0],
282 # [-3, 0, 3, 0], [1, 4, 1, 0]]
283 # My own invention to try to get something that somewhat acts right
284 # This is semi-quadratic rather than fully cubic:
285 poly = [[0, -1, 0, 1], [1, -2, 1, 0],
286 [0, -1, 2, 0], [1, -1, 0, 0]]
288 if include_ends:
289 pieces.append(p1_co)
291 # Generate each point:
292 for i in range(segments - 1):
293 t = fraction * (i + 1)
294 if ENABLE_DEBUG:
295 print(t)
296 s = [t ** 3, t ** 2, t, 1]
297 h00 = (poly[0][0] * s[0]) + (poly[0][1] * s[1]) + (poly[0][2] * s[2]) + (poly[0][3] * s[3])
298 h01 = (poly[1][0] * s[0]) + (poly[1][1] * s[1]) + (poly[1][2] * s[2]) + (poly[1][3] * s[3])
299 h10 = (poly[2][0] * s[0]) + (poly[2][1] * s[1]) + (poly[2][2] * s[2]) + (poly[2][3] * s[3])
300 h11 = (poly[3][0] * s[0]) + (poly[3][1] * s[1]) + (poly[3][2] * s[2]) + (poly[3][3] * s[3])
301 pieces.append((h00 * p1_co) + (h01 * p1_dir) + (h10 * p2_co) + (h11 * p2_dir))
302 if include_ends:
303 pieces.append(p2_co)
305 # Return:
306 if len(pieces) == 0:
307 return None
308 else:
309 if ENABLE_DEBUG:
310 print(pieces)
311 return pieces
314 # intersect_line_face
316 # Calculates the coordinate of intersection of a line with a face. It returns
317 # the coordinate if one exists, otherwise None. It can only deal with tris or
318 # quads for a face. A quad does NOT have to be planar
320 Quad math and theory:
321 A quad may not be planar. Therefore the treated definition of the surface is
322 that the surface is composed of all lines bridging two other lines defined by
323 the given four points. The lines do not "cross"
325 The two lines in 3-space can defined as:
326 ┌ ┐ ┌ ┐ ┌ ┐ ┌ ┐ ┌ ┐ ┌ ┐
327 │x1│ │a11│ │b11│ │x2│ │a21│ │b21│
328 │y1│ = (1-t1)│a12│ + t1│b12│, │y2│ = (1-t2)│a22│ + t2│b22│
329 │z1│ │a13│ │b13│ │z2│ │a23│ │b23│
330 └ ┘ └ ┘ └ ┘ └ ┘ └ ┘ └ ┘
331 Therefore, the surface is the lines defined by every point alone the two
332 lines with a same "t" value (t1 = t2). This is basically R = V1 + tQ, where
333 Q = V2 - V1 therefore R = V1 + t(V2 - V1) -> R = (1 - t)V1 + tV2:
334 ┌ ┐ ┌ ┐ ┌ ┐
335 │x12│ │(1-t)a11 + t * b11│ │(1-t)a21 + t * b21│
336 │y12│ = (1 - t12)│(1-t)a12 + t * b12│ + t12│(1-t)a22 + t * b22│
337 │z12│ │(1-t)a13 + t * b13│ │(1-t)a23 + t * b23│
338 └ ┘ └ ┘ └ ┘
339 Now, the equation of our line can be likewise defined:
340 ┌ ┐ ┌ ┐ ┌ ┐
341 │x3│ │a31│ │b31│
342 │y3│ = │a32│ + t3│b32│
343 │z3│ │a33│ │b33│
344 └ ┘ └ ┘ └ ┘
345 Now we just have to find a valid solution for the two equations. This should
346 be our point of intersection. Therefore, x12 = x3 -> x, y12 = y3 -> y,
347 z12 = z3 -> z. Thus, to find that point we set the equation defining the
348 surface as equal to the equation for the line:
349 ┌ ┐ ┌ ┐ ┌ ┐ ┌ ┐
350 │(1-t)a11 + t * b11│ │(1-t)a21 + t * b21│ │a31│ │b31│
351 (1 - t12)│(1-t)a12 + t * b12│ + t12│(1-t)a22 + t * b22│ = │a32│ + t3│b32│
352 │(1-t)a13 + t * b13│ │(1-t)a23 + t * b23│ │a33│ │b33│
353 └ ┘ └ ┘ └ ┘ └ ┘
354 This leaves us with three equations, three unknowns. Solving the system by
355 hand is practically impossible, but using Mathematica we are given an insane
356 series of three equations (not reproduced here for the sake of space: see
357 http://www.mediafire.com/file/cc6m6ba3sz2b96m/intersect_line_surface.nb and
358 http://www.mediafire.com/file/0egbr5ahg14talm/intersect_line_surface2.nb for
359 Mathematica computation).
361 Additionally, the resulting series of equations may result in a div by zero
362 exception if the line in question if parallel to one of the axis or if the
363 quad is planar and parallel to either the XY, XZ, or YZ planes. However, the
364 system is still solvable but must be dealt with a little differently to avaid
365 these special cases. Because the resulting equations are a little different,
366 we have to code them differently. 00Hence the special cases.
368 Tri math and theory:
369 A triangle must be planar (three points define a plane). So we just
370 have to make sure that the line intersects inside the triangle.
372 If the point is within the triangle, then the angle between the lines that
373 connect the point to the each individual point of the triangle will be
374 equal to 2 * PI. Otherwise, if the point is outside the triangle, then the
375 sum of the angles will be less.
377 # @todo
378 # - Figure out how to deal with n-gons
379 # How the heck is a face with 8 verts defined mathematically?
380 # How do I then find the intersection point of a line with said vert?
381 # How do I know if that point is "inside" all the verts?
382 # I have no clue, and haven't been able to find anything on it so far
383 # Maybe if someone (actually reads this and) who knows could note?
386 def intersect_line_face(edge, face, is_infinite=False, error=0.000002):
387 int_co = None
389 # If we are dealing with a non-planar quad:
390 if len(face.verts) == 4 and not is_face_planar(face):
391 edgeA = face.edges[0]
392 edgeB = None
393 flipB = False
395 for i in range(len(face.edges)):
396 if face.edges[i].verts[0] not in edgeA.verts and \
397 face.edges[i].verts[1] not in edgeA.verts:
399 edgeB = face.edges[i]
400 break
402 # I haven't figured out a way to mix this in with the above. Doing so might remove a
403 # few extra instructions from having to be executed saving a few clock cycles:
404 for i in range(len(face.edges)):
405 if face.edges[i] == edgeA or face.edges[i] == edgeB:
406 continue
407 if ((edgeA.verts[0] in face.edges[i].verts and
408 edgeB.verts[1] in face.edges[i].verts) or
409 (edgeA.verts[1] in face.edges[i].verts and edgeB.verts[0] in face.edges[i].verts)):
411 flipB = True
412 break
414 # Define calculation coefficient constants:
415 # "xx1" is the x coordinate, "xx2" is the y coordinate, and "xx3" is the z coordinate
416 a11, a12, a13 = edgeA.verts[0].co[0], edgeA.verts[0].co[1], edgeA.verts[0].co[2]
417 b11, b12, b13 = edgeA.verts[1].co[0], edgeA.verts[1].co[1], edgeA.verts[1].co[2]
419 if flipB:
420 a21, a22, a23 = edgeB.verts[1].co[0], edgeB.verts[1].co[1], edgeB.verts[1].co[2]
421 b21, b22, b23 = edgeB.verts[0].co[0], edgeB.verts[0].co[1], edgeB.verts[0].co[2]
422 else:
423 a21, a22, a23 = edgeB.verts[0].co[0], edgeB.verts[0].co[1], edgeB.verts[0].co[2]
424 b21, b22, b23 = edgeB.verts[1].co[0], edgeB.verts[1].co[1], edgeB.verts[1].co[2]
425 a31, a32, a33 = edge.verts[0].co[0], edge.verts[0].co[1], edge.verts[0].co[2]
426 b31, b32, b33 = edge.verts[1].co[0], edge.verts[1].co[1], edge.verts[1].co[2]
428 # There are a bunch of duplicate "sub-calculations" inside the resulting
429 # equations for t, t12, and t3. Calculate them once and store them to
430 # reduce computational time:
431 m01 = a13 * a22 * a31
432 m02 = a12 * a23 * a31
433 m03 = a13 * a21 * a32
434 m04 = a11 * a23 * a32
435 m05 = a12 * a21 * a33
436 m06 = a11 * a22 * a33
437 m07 = a23 * a32 * b11
438 m08 = a22 * a33 * b11
439 m09 = a23 * a31 * b12
440 m10 = a21 * a33 * b12
441 m11 = a22 * a31 * b13
442 m12 = a21 * a32 * b13
443 m13 = a13 * a32 * b21
444 m14 = a12 * a33 * b21
445 m15 = a13 * a31 * b22
446 m16 = a11 * a33 * b22
447 m17 = a12 * a31 * b23
448 m18 = a11 * a32 * b23
449 m19 = a13 * a22 * b31
450 m20 = a12 * a23 * b31
451 m21 = a13 * a32 * b31
452 m22 = a23 * a32 * b31
453 m23 = a12 * a33 * b31
454 m24 = a22 * a33 * b31
455 m25 = a23 * b12 * b31
456 m26 = a33 * b12 * b31
457 m27 = a22 * b13 * b31
458 m28 = a32 * b13 * b31
459 m29 = a13 * b22 * b31
460 m30 = a33 * b22 * b31
461 m31 = a12 * b23 * b31
462 m32 = a32 * b23 * b31
463 m33 = a13 * a21 * b32
464 m34 = a11 * a23 * b32
465 m35 = a13 * a31 * b32
466 m36 = a23 * a31 * b32
467 m37 = a11 * a33 * b32
468 m38 = a21 * a33 * b32
469 m39 = a23 * b11 * b32
470 m40 = a33 * b11 * b32
471 m41 = a21 * b13 * b32
472 m42 = a31 * b13 * b32
473 m43 = a13 * b21 * b32
474 m44 = a33 * b21 * b32
475 m45 = a11 * b23 * b32
476 m46 = a31 * b23 * b32
477 m47 = a12 * a21 * b33
478 m48 = a11 * a22 * b33
479 m49 = a12 * a31 * b33
480 m50 = a22 * a31 * b33
481 m51 = a11 * a32 * b33
482 m52 = a21 * a32 * b33
483 m53 = a22 * b11 * b33
484 m54 = a32 * b11 * b33
485 m55 = a21 * b12 * b33
486 m56 = a31 * b12 * b33
487 m57 = a12 * b21 * b33
488 m58 = a32 * b21 * b33
489 m59 = a11 * b22 * b33
490 m60 = a31 * b22 * b33
491 m61 = a33 * b12 * b21
492 m62 = a32 * b13 * b21
493 m63 = a33 * b11 * b22
494 m64 = a31 * b13 * b22
495 m65 = a32 * b11 * b23
496 m66 = a31 * b12 * b23
497 m67 = b13 * b22 * b31
498 m68 = b12 * b23 * b31
499 m69 = b13 * b21 * b32
500 m70 = b11 * b23 * b32
501 m71 = b12 * b21 * b33
502 m72 = b11 * b22 * b33
503 n01 = m01 - m02 - m03 + m04 + m05 - m06
504 n02 = -m07 + m08 + m09 - m10 - m11 + m12 + m13 - m14 - m15 + m16 + m17 - m18 - \
505 m25 + m27 + m29 - m31 + m39 - m41 - m43 + m45 - m53 + m55 + m57 - m59
506 n03 = -m19 + m20 + m33 - m34 - m47 + m48
507 n04 = m21 - m22 - m23 + m24 - m35 + m36 + m37 - m38 + m49 - m50 - m51 + m52
508 n05 = m26 - m28 - m30 + m32 - m40 + m42 + m44 - m46 + m54 - m56 - m58 + m60
509 n06 = m61 - m62 - m63 + m64 + m65 - m66 - m67 + m68 + m69 - m70 - m71 + m72
510 n07 = 2 * n01 + n02 + 2 * n03 + n04 + n05
511 n08 = n01 + n02 + n03 + n06
513 # Calculate t, t12, and t3:
514 t = (n07 - sqrt(pow(-n07, 2) - 4 * (n01 + n03 + n04) * n08)) / (2 * n08)
516 # t12 can be greatly simplified by defining it with t in it:
517 # If block used to help prevent any div by zero error.
518 t12 = 0
520 if a31 == b31:
521 # The line is parallel to the z-axis:
522 if a32 == b32:
523 t12 = ((a11 - a31) + (b11 - a11) * t) / ((a21 - a11) + (a11 - a21 - b11 + b21) * t)
524 # The line is parallel to the y-axis:
525 elif a33 == b33:
526 t12 = ((a11 - a31) + (b11 - a11) * t) / ((a21 - a11) + (a11 - a21 - b11 + b21) * t)
527 # The line is along the y/z-axis but is not parallel to either:
528 else:
529 t12 = -(-(a33 - b33) * (-a32 + a12 * (1 - t) + b12 * t) + (a32 - b32) *
530 (-a33 + a13 * (1 - t) + b13 * t)) / (-(a33 - b33) *
531 ((a22 - a12) * (1 - t) + (b22 - b12) * t) + (a32 - b32) *
532 ((a23 - a13) * (1 - t) + (b23 - b13) * t))
533 elif a32 == b32:
534 # The line is parallel to the x-axis:
535 if a33 == b33:
536 t12 = ((a12 - a32) + (b12 - a12) * t) / ((a22 - a12) + (a12 - a22 - b12 + b22) * t)
537 # The line is along the x/z-axis but is not parallel to either:
538 else:
539 t12 = -(-(a33 - b33) * (-a31 + a11 * (1 - t) + b11 * t) + (a31 - b31) * (-a33 + a13 *
540 (1 - t) + b13 * t)) / (-(a33 - b33) * ((a21 - a11) * (1 - t) + (b21 - b11) * t) +
541 (a31 - b31) * ((a23 - a13) * (1 - t) + (b23 - b13) * t))
542 # The line is along the x/y-axis but is not parallel to either:
543 else:
544 t12 = -(-(a32 - b32) * (-a31 + a11 * (1 - t) + b11 * t) + (a31 - b31) * (-a32 + a12 *
545 (1 - t) + b12 * t)) / (-(a32 - b32) * ((a21 - a11) * (1 - t) + (b21 - b11) * t) +
546 (a31 - b31) * ((a22 - a21) * (1 - t) + (b22 - b12) * t))
548 # Likewise, t3 is greatly simplified by defining it in terms of t and t12:
549 # If block used to prevent a div by zero error.
550 t3 = 0
551 if a31 != b31:
552 t3 = (-a11 + a31 + (a11 - b11) * t + (a11 - a21) *
553 t12 + (a21 - a11 + b11 - b21) * t * t12) / (a31 - b31)
554 elif a32 != b32:
555 t3 = (-a12 + a32 + (a12 - b12) * t + (a12 - a22) *
556 t12 + (a22 - a12 + b12 - b22) * t * t12) / (a32 - b32)
557 elif a33 != b33:
558 t3 = (-a13 + a33 + (a13 - b13) * t + (a13 - a23) *
559 t12 + (a23 - a13 + b13 - b23) * t * t12) / (a33 - b33)
560 else:
561 if ENABLE_DEBUG:
562 print("The second edge is a zero-length edge")
563 return None
565 # Calculate the point of intersection:
566 x = (1 - t3) * a31 + t3 * b31
567 y = (1 - t3) * a32 + t3 * b32
568 z = (1 - t3) * a33 + t3 * b33
569 int_co = Vector((x, y, z))
571 if ENABLE_DEBUG:
572 print(int_co)
574 # If the line does not intersect the quad, we return "None":
575 if (t < -1 or t > 1 or t12 < -1 or t12 > 1) and not is_infinite:
576 int_co = None
578 elif len(face.verts) == 3:
579 p1, p2, p3 = face.verts[0].co, face.verts[1].co, face.verts[2].co
580 int_co = intersect_line_plane(edge.verts[0].co, edge.verts[1].co, p1, face.normal)
582 # Only check if the triangle is not being treated as an infinite plane:
583 # Math based from http://paulbourke.net/geometry/linefacet/
584 if int_co is not None and not is_infinite:
585 pA = p1 - int_co
586 pB = p2 - int_co
587 pC = p3 - int_co
588 # These must be unit vectors, else we risk a domain error:
589 pA.length = 1
590 pB.length = 1
591 pC.length = 1
592 aAB = acos(pA.dot(pB))
593 aBC = acos(pB.dot(pC))
594 aCA = acos(pC.dot(pA))
595 sumA = aAB + aBC + aCA
597 # If the point is outside the triangle:
598 if (sumA > (pi + error) and sumA < (pi - error)):
599 int_co = None
601 # This is the default case where we either have a planar quad or an n-gon
602 else:
603 int_co = intersect_line_plane(edge.verts[0].co, edge.verts[1].co,
604 face.verts[0].co, face.normal)
605 return int_co
608 # project_point_plane
609 # Projects a point onto a plane. Returns a tuple of the projection vector
610 # and the projected coordinate
612 def project_point_plane(pt, plane_co, plane_no):
613 if ENABLE_DEBUG:
614 print("project_point_plane was called")
615 proj_co = intersect_line_plane(pt, pt + plane_no, plane_co, plane_no)
616 proj_ve = proj_co - pt
617 if ENABLE_DEBUG:
618 print("project_point_plane: proj_co is {}\nproj_ve is {}".format(proj_co, proj_ve))
619 return (proj_ve, proj_co)
622 # ------------ CHAMPHER HELPER METHODS -------------
624 def is_planar_edge(edge, error=0.000002):
625 angle = edge.calc_face_angle()
626 return ((angle < error and angle > -error) or
627 (angle < (180 + error) and angle > (180 - error)))
630 # ------------- EDGE TOOL METHODS -------------------
632 # Extends an "edge" in two directions:
633 # - Requires two vertices to be selected. They do not have to form an edge
634 # - Extends "length" in both directions
636 class Extend(Operator):
637 bl_idname = "mesh.edgetools_extend"
638 bl_label = "Extend"
639 bl_description = "Extend the selected edges of vertex pairs"
640 bl_options = {'REGISTER', 'UNDO'}
642 di1: BoolProperty(
643 name="Forwards",
644 description="Extend the edge forwards",
645 default=True
647 di2: BoolProperty(
648 name="Backwards",
649 description="Extend the edge backwards",
650 default=False
652 length: FloatProperty(
653 name="Length",
654 description="Length to extend the edge",
655 min=0.0, max=1024.0,
656 default=1.0
659 def draw(self, context):
660 layout = self.layout
662 row = layout.row(align=True)
663 row.prop(self, "di1", toggle=True)
664 row.prop(self, "di2", toggle=True)
666 layout.prop(self, "length")
668 @classmethod
669 def poll(cls, context):
670 ob = context.active_object
671 return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
673 def invoke(self, context, event):
674 return self.execute(context)
676 def execute(self, context):
677 try:
678 me = context.object.data
679 bm = bmesh.from_edit_mesh(me)
680 bm.normal_update()
682 bEdges = bm.edges
683 bVerts = bm.verts
685 edges = [e for e in bEdges if e.select]
686 verts = [v for v in bVerts if v.select]
688 if not is_selected_enough(self, edges, 0, edges_n=1, verts_n=0, types="Edge"):
689 return {'CANCELLED'}
691 if len(edges) > 0:
692 for e in edges:
693 vector = e.verts[0].co - e.verts[1].co
694 vector.length = self.length
696 if self.di1:
697 v = bVerts.new()
698 if (vector[0] + vector[1] + vector[2]) < 0:
699 v.co = e.verts[1].co - vector
700 newE = bEdges.new((e.verts[1], v))
701 bEdges.ensure_lookup_table()
702 else:
703 v.co = e.verts[0].co + vector
704 newE = bEdges.new((e.verts[0], v))
705 bEdges.ensure_lookup_table()
706 if self.di2:
707 v = bVerts.new()
708 if (vector[0] + vector[1] + vector[2]) < 0:
709 v.co = e.verts[0].co + vector
710 newE = bEdges.new((e.verts[0], v))
711 bEdges.ensure_lookup_table()
712 else:
713 v.co = e.verts[1].co - vector
714 newE = bEdges.new((e.verts[1], v))
715 bEdges.ensure_lookup_table()
716 else:
717 vector = verts[0].co - verts[1].co
718 vector.length = self.length
720 if self.di1:
721 v = bVerts.new()
722 if (vector[0] + vector[1] + vector[2]) < 0:
723 v.co = verts[1].co - vector
724 e = bEdges.new((verts[1], v))
725 bEdges.ensure_lookup_table()
726 else:
727 v.co = verts[0].co + vector
728 e = bEdges.new((verts[0], v))
729 bEdges.ensure_lookup_table()
730 if self.di2:
731 v = bVerts.new()
732 if (vector[0] + vector[1] + vector[2]) < 0:
733 v.co = verts[0].co + vector
734 e = bEdges.new((verts[0], v))
735 bEdges.ensure_lookup_table()
736 else:
737 v.co = verts[1].co - vector
738 e = bEdges.new((verts[1], v))
739 bEdges.ensure_lookup_table()
741 bmesh.update_edit_mesh(me)
743 except Exception as e:
744 error_handlers(self, "mesh.edgetools_extend", e,
745 reports="Extend Operator failed", func=False)
746 return {'CANCELLED'}
748 return {'FINISHED'}
751 # Creates a series of edges between two edges using spline interpolation.
752 # This basically just exposes existing functionality in addition to some
753 # other common methods: Hermite (c-spline), Bezier, and b-spline. These
754 # alternates I coded myself after some extensive research into spline theory
756 # @todo Figure out what's wrong with the Blender bezier interpolation
758 class Spline(Operator):
759 bl_idname = "mesh.edgetools_spline"
760 bl_label = "Spline"
761 bl_description = "Create a spline interplopation between two edges"
762 bl_options = {'REGISTER', 'UNDO'}
764 alg: EnumProperty(
765 name="Spline Algorithm",
766 items=[('Blender', "Blender", "Interpolation provided through mathutils.geometry"),
767 ('Hermite', "C-Spline", "C-spline interpolation"),
768 ('Bezier', "Bezier", "Bezier interpolation"),
769 ('B-Spline', "B-Spline", "B-Spline interpolation")],
770 default='Bezier'
772 segments: IntProperty(
773 name="Segments",
774 description="Number of segments to use in the interpolation",
775 min=2, max=4096,
776 soft_max=1024,
777 default=32
779 flip1: BoolProperty(
780 name="Flip Edge",
781 description="Flip the direction of the spline on Edge 1",
782 default=False
784 flip2: BoolProperty(
785 name="Flip Edge",
786 description="Flip the direction of the spline on Edge 2",
787 default=False
789 ten1: FloatProperty(
790 name="Tension",
791 description="Tension on Edge 1",
792 min=-4096.0, max=4096.0,
793 soft_min=-8.0, soft_max=8.0,
794 default=1.0
796 ten2: FloatProperty(
797 name="Tension",
798 description="Tension on Edge 2",
799 min=-4096.0, max=4096.0,
800 soft_min=-8.0, soft_max=8.0,
801 default=1.0
804 def draw(self, context):
805 layout = self.layout
807 layout.prop(self, "alg")
808 layout.prop(self, "segments")
810 layout.label(text="Edge 1:")
811 split = layout.split(factor=0.8, align=True)
812 split.prop(self, "ten1")
813 split.prop(self, "flip1", text="Flip1", toggle=True)
815 layout.label(text="Edge 2:")
816 split = layout.split(factor=0.8, align=True)
817 split.prop(self, "ten2")
818 split.prop(self, "flip2", text="Flip2", toggle=True)
820 @classmethod
821 def poll(cls, context):
822 ob = context.active_object
823 return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
825 def invoke(self, context, event):
826 return self.execute(context)
828 def execute(self, context):
829 try:
830 me = context.object.data
831 bm = bmesh.from_edit_mesh(me)
832 bm.normal_update()
834 bEdges = bm.edges
835 bVerts = bm.verts
837 seg = self.segments
838 edges = [e for e in bEdges if e.select]
840 if not is_selected_enough(self, edges, 0, edges_n=2, verts_n=0, types="Edge"):
841 return {'CANCELLED'}
843 verts = [edges[v // 2].verts[v % 2] for v in range(4)]
845 if self.flip1:
846 v1 = verts[1]
847 p1_co = verts[1].co
848 p1_dir = verts[1].co - verts[0].co
849 else:
850 v1 = verts[0]
851 p1_co = verts[0].co
852 p1_dir = verts[0].co - verts[1].co
853 if self.ten1 < 0:
854 p1_dir = -1 * p1_dir
855 p1_dir.length = -self.ten1
856 else:
857 p1_dir.length = self.ten1
859 if self.flip2:
860 v2 = verts[3]
861 p2_co = verts[3].co
862 p2_dir = verts[2].co - verts[3].co
863 else:
864 v2 = verts[2]
865 p2_co = verts[2].co
866 p2_dir = verts[3].co - verts[2].co
867 if self.ten2 < 0:
868 p2_dir = -1 * p2_dir
869 p2_dir.length = -self.ten2
870 else:
871 p2_dir.length = self.ten2
873 # Get the interploted coordinates:
874 if self.alg == 'Blender':
875 pieces = interpolate_bezier(
876 p1_co, p1_dir, p2_dir, p2_co, self.segments
878 elif self.alg == 'Hermite':
879 pieces = interpolate_line_line(
880 p1_co, p1_dir, p2_co, p2_dir, self.segments, 1, 'HERMITE'
882 elif self.alg == 'Bezier':
883 pieces = interpolate_line_line(
884 p1_co, p1_dir, p2_co, p2_dir, self.segments, 1, 'BEZIER'
886 elif self.alg == 'B-Spline':
887 pieces = interpolate_line_line(
888 p1_co, p1_dir, p2_co, p2_dir, self.segments, 1, 'BSPLINE'
891 verts = []
892 verts.append(v1)
893 # Add vertices and set the points:
894 for i in range(seg - 1):
895 v = bVerts.new()
896 v.co = pieces[i]
897 bVerts.ensure_lookup_table()
898 verts.append(v)
899 verts.append(v2)
900 # Connect vertices:
901 for i in range(seg):
902 e = bEdges.new((verts[i], verts[i + 1]))
903 bEdges.ensure_lookup_table()
905 bmesh.update_edit_mesh(me)
907 except Exception as e:
908 error_handlers(self, "mesh.edgetools_spline", e,
909 reports="Spline Operator failed", func=False)
910 return {'CANCELLED'}
912 return {'FINISHED'}
915 # Creates edges normal to planes defined between each of two edges and the
916 # normal or the plane defined by those two edges.
917 # - Select two edges. The must form a plane.
918 # - On running the script, eight edges will be created. Delete the
919 # extras that you don't need.
920 # - The length of those edges is defined by the variable "length"
922 # @todo Change method from a cross product to a rotation matrix to make the
923 # angle part work.
924 # --- todo completed 2/4/2012, but still needs work ---
925 # @todo Figure out a way to make +/- predictable
926 # - Maybe use angle between edges and vector direction definition?
927 # --- TODO COMPLETED ON 2/9/2012 ---
929 class Ortho(Operator):
930 bl_idname = "mesh.edgetools_ortho"
931 bl_label = "Angle Off Edge"
932 bl_description = "Creates new edges within an angle from vertices of selected edges"
933 bl_options = {'REGISTER', 'UNDO'}
935 vert1: BoolProperty(
936 name="Vertice 1",
937 description="Enable edge creation for Vertice 1",
938 default=True
940 vert2: BoolProperty(
941 name="Vertice 2",
942 description="Enable edge creation for Vertice 2",
943 default=True
945 vert3: BoolProperty(
946 name="Vertice 3",
947 description="Enable edge creation for Vertice 3",
948 default=True
950 vert4: BoolProperty(
951 name="Vertice 4",
952 description="Enable edge creation for Vertice 4",
953 default=True
955 pos: BoolProperty(
956 name="Positive",
957 description="Enable creation of positive direction edges",
958 default=True
960 neg: BoolProperty(
961 name="Negative",
962 description="Enable creation of negative direction edges",
963 default=True
965 angle: FloatProperty(
966 name="Angle",
967 description="Define the angle off of the originating edge",
968 min=0.0, max=180.0,
969 default=90.0
971 length: FloatProperty(
972 name="Length",
973 description="Length of created edges",
974 min=0.0, max=1024.0,
975 default=1.0
977 # For when only one edge is selected (Possible feature to be testd):
978 plane: EnumProperty(
979 name="Plane",
980 items=[("XY", "X-Y Plane", "Use the X-Y plane as the plane of creation"),
981 ("XZ", "X-Z Plane", "Use the X-Z plane as the plane of creation"),
982 ("YZ", "Y-Z Plane", "Use the Y-Z plane as the plane of creation")],
983 default="XY"
986 def draw(self, context):
987 layout = self.layout
989 layout.label(text="Creation:")
990 split = layout.split()
991 col = split.column()
993 col.prop(self, "vert1", toggle=True)
994 col.prop(self, "vert2", toggle=True)
996 col = split.column()
997 col.prop(self, "vert3", toggle=True)
998 col.prop(self, "vert4", toggle=True)
1000 layout.label(text="Direction:")
1001 row = layout.row(align=False)
1002 row.alignment = 'EXPAND'
1003 row.prop(self, "pos")
1004 row.prop(self, "neg")
1006 layout.separator()
1008 col = layout.column(align=True)
1009 col.prop(self, "angle")
1010 col.prop(self, "length")
1012 @classmethod
1013 def poll(cls, context):
1014 ob = context.active_object
1015 return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
1017 def invoke(self, context, event):
1018 return self.execute(context)
1020 def execute(self, context):
1021 try:
1022 me = context.object.data
1023 bm = bmesh.from_edit_mesh(me)
1024 bm.normal_update()
1026 bVerts = bm.verts
1027 bEdges = bm.edges
1028 edges = [e for e in bEdges if e.select]
1029 vectors = []
1031 if not is_selected_enough(self, edges, 0, edges_n=2, verts_n=0, types="Edge"):
1032 return {'CANCELLED'}
1034 verts = [edges[0].verts[0],
1035 edges[0].verts[1],
1036 edges[1].verts[0],
1037 edges[1].verts[1]]
1039 cos = intersect_line_line(verts[0].co, verts[1].co, verts[2].co, verts[3].co)
1041 # If the two edges are parallel:
1042 if cos is None:
1043 self.report({'WARNING'},
1044 "Selected lines are parallel: results may be unpredictable")
1045 vectors.append(verts[0].co - verts[1].co)
1046 vectors.append(verts[0].co - verts[2].co)
1047 vectors.append(vectors[0].cross(vectors[1]))
1048 vectors.append(vectors[2].cross(vectors[0]))
1049 vectors.append(-vectors[3])
1050 else:
1051 # Warn the user if they have not chosen two planar edges:
1052 if not is_same_co(cos[0], cos[1]):
1053 self.report({'WARNING'},
1054 "Selected lines are not planar: results may be unpredictable")
1056 # This makes the +/- behavior predictable:
1057 if (verts[0].co - cos[0]).length < (verts[1].co - cos[0]).length:
1058 verts[0], verts[1] = verts[1], verts[0]
1059 if (verts[2].co - cos[0]).length < (verts[3].co - cos[0]).length:
1060 verts[2], verts[3] = verts[3], verts[2]
1062 vectors.append(verts[0].co - verts[1].co)
1063 vectors.append(verts[2].co - verts[3].co)
1065 # Normal of the plane formed by vector1 and vector2:
1066 vectors.append(vectors[0].cross(vectors[1]))
1068 # Possible directions:
1069 vectors.append(vectors[2].cross(vectors[0]))
1070 vectors.append(vectors[1].cross(vectors[2]))
1072 # Set the length:
1073 vectors[3].length = self.length
1074 vectors[4].length = self.length
1076 # Perform any additional rotations:
1077 matrix = Matrix.Rotation(radians(90 + self.angle), 3, vectors[2])
1078 vectors.append(matrix @ -vectors[3]) # vectors[5]
1079 matrix = Matrix.Rotation(radians(90 - self.angle), 3, vectors[2])
1080 vectors.append(matrix @ vectors[4]) # vectors[6]
1081 vectors.append(matrix @ vectors[3]) # vectors[7]
1082 matrix = Matrix.Rotation(radians(90 + self.angle), 3, vectors[2])
1083 vectors.append(matrix @ -vectors[4]) # vectors[8]
1085 # Perform extrusions and displacements:
1086 # There will be a total of 8 extrusions. One for each vert of each edge.
1087 # It looks like an extrusion will add the new vert to the end of the verts
1088 # list and leave the rest in the same location.
1089 # -- EDIT --
1090 # It looks like I might be able to do this within "bpy.data" with the ".add" function
1092 for v in range(len(verts)):
1093 vert = verts[v]
1094 if ((v == 0 and self.vert1) or (v == 1 and self.vert2) or
1095 (v == 2 and self.vert3) or (v == 3 and self.vert4)):
1097 if self.pos:
1098 new = bVerts.new()
1099 new.co = vert.co - vectors[5 + (v // 2) + ((v % 2) * 2)]
1100 bVerts.ensure_lookup_table()
1101 bEdges.new((vert, new))
1102 bEdges.ensure_lookup_table()
1103 if self.neg:
1104 new = bVerts.new()
1105 new.co = vert.co + vectors[5 + (v // 2) + ((v % 2) * 2)]
1106 bVerts.ensure_lookup_table()
1107 bEdges.new((vert, new))
1108 bEdges.ensure_lookup_table()
1110 bmesh.update_edit_mesh(me)
1111 except Exception as e:
1112 error_handlers(self, "mesh.edgetools_ortho", e,
1113 reports="Angle Off Edge Operator failed", func=False)
1114 return {'CANCELLED'}
1116 return {'FINISHED'}
1119 # Usage:
1120 # Select an edge and a point or an edge and specify the radius (default is 1 BU)
1121 # You can select two edges but it might be unpredictable which edge it revolves
1122 # around so you might have to play with the switch
1124 class Shaft(Operator):
1125 bl_idname = "mesh.edgetools_shaft"
1126 bl_label = "Shaft"
1127 bl_description = "Create a shaft mesh around an axis"
1128 bl_options = {'REGISTER', 'UNDO'}
1130 # Selection defaults:
1131 shaftType = 0
1133 # For tracking if the user has changed selection:
1134 last_edge: IntProperty(
1135 name="Last Edge",
1136 description="Tracks if user has changed selected edges",
1137 min=0, max=1,
1138 default=0
1140 last_flip = False
1142 edge: IntProperty(
1143 name="Edge",
1144 description="Edge to shaft around",
1145 min=0, max=1,
1146 default=0
1148 flip: BoolProperty(
1149 name="Flip Second Edge",
1150 description="Flip the perceived direction of the second edge",
1151 default=False
1153 radius: FloatProperty(
1154 name="Radius",
1155 description="Shaft Radius",
1156 min=0.0, max=1024.0,
1157 default=1.0
1159 start: FloatProperty(
1160 name="Starting Angle",
1161 description="Angle to start the shaft at",
1162 min=-360.0, max=360.0,
1163 default=0.0
1165 finish: FloatProperty(
1166 name="Ending Angle",
1167 description="Angle to end the shaft at",
1168 min=-360.0, max=360.0,
1169 default=360.0
1171 segments: IntProperty(
1172 name="Shaft Segments",
1173 description="Number of segments to use in the shaft",
1174 min=1, max=4096,
1175 soft_max=512,
1176 default=32
1179 def draw(self, context):
1180 layout = self.layout
1182 if self.shaftType == 0:
1183 layout.prop(self, "edge")
1184 layout.prop(self, "flip")
1185 elif self.shaftType == 3:
1186 layout.prop(self, "radius")
1188 layout.prop(self, "segments")
1189 layout.prop(self, "start")
1190 layout.prop(self, "finish")
1192 @classmethod
1193 def poll(cls, context):
1194 ob = context.active_object
1195 return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
1197 def invoke(self, context, event):
1198 # Make sure these get reset each time we run:
1199 self.last_edge = 0
1200 self.edge = 0
1202 return self.execute(context)
1204 def execute(self, context):
1205 try:
1206 me = context.object.data
1207 bm = bmesh.from_edit_mesh(me)
1208 bm.normal_update()
1210 bFaces = bm.faces
1211 bEdges = bm.edges
1212 bVerts = bm.verts
1214 active = None
1215 edges, verts = [], []
1217 # Pre-caclulated values:
1218 rotRange = [radians(self.start), radians(self.finish)]
1219 rads = radians((self.finish - self.start) / self.segments)
1221 numV = self.segments + 1
1222 numE = self.segments
1224 edges = [e for e in bEdges if e.select]
1226 # Robustness check: there should at least be one edge selected
1227 if not is_selected_enough(self, edges, 0, edges_n=1, verts_n=0, types="Edge"):
1228 return {'CANCELLED'}
1230 # If two edges are selected:
1231 if len(edges) == 2:
1232 # default:
1233 edge = [0, 1]
1234 vert = [0, 1]
1236 # By default, we want to shaft around the last selected edge (it
1237 # will be the active edge). We know we are using the default if
1238 # the user has not changed which edge is being shafted around (as
1239 # is tracked by self.last_edge). When they are not the same, then
1240 # the user has changed selection.
1241 # We then need to make sure that the active object really is an edge
1242 # (robustness check)
1243 # Finally, if the active edge is not the initial one, we flip them
1244 # and have the GUI reflect that
1245 if self.last_edge == self.edge:
1246 if isinstance(bm.select_history.active, bmesh.types.BMEdge):
1247 if bm.select_history.active != edges[edge[0]]:
1248 self.last_edge, self.edge = edge[1], edge[1]
1249 edge = [edge[1], edge[0]]
1250 else:
1251 flip_edit_mode()
1252 self.report({'WARNING'},
1253 "Active geometry is not an edge. Operation Cancelled")
1254 return {'CANCELLED'}
1255 elif self.edge == 1:
1256 edge = [1, 0]
1258 verts.append(edges[edge[0]].verts[0])
1259 verts.append(edges[edge[0]].verts[1])
1261 if self.flip:
1262 verts = [1, 0]
1264 verts.append(edges[edge[1]].verts[vert[0]])
1265 verts.append(edges[edge[1]].verts[vert[1]])
1267 self.shaftType = 0
1268 # If there is more than one edge selected:
1269 # There are some issues with it ATM, so don't expose is it to normal users
1270 # @todo Fix edge connection ordering issue
1271 elif ENABLE_DEBUG and len(edges) > 2:
1272 if isinstance(bm.select_history.active, bmesh.types.BMEdge):
1273 active = bm.select_history.active
1274 edges.remove(active)
1275 # Get all the verts:
1276 # edges = order_joined_edges(edges[0])
1277 verts = []
1278 for e in edges:
1279 if verts.count(e.verts[0]) == 0:
1280 verts.append(e.verts[0])
1281 if verts.count(e.verts[1]) == 0:
1282 verts.append(e.verts[1])
1283 else:
1284 flip_edit_mode()
1285 self.report({'WARNING'},
1286 "Active geometry is not an edge. Operation Cancelled")
1287 return {'CANCELLED'}
1288 self.shaftType = 1
1289 else:
1290 verts.append(edges[0].verts[0])
1291 verts.append(edges[0].verts[1])
1293 for v in bVerts:
1294 if v.select and verts.count(v) == 0:
1295 verts.append(v)
1296 v.select = False
1297 if len(verts) == 2:
1298 self.shaftType = 3
1299 else:
1300 self.shaftType = 2
1302 # The vector denoting the axis of rotation:
1303 if self.shaftType == 1:
1304 axis = active.verts[1].co - active.verts[0].co
1305 else:
1306 axis = verts[1].co - verts[0].co
1308 # We will need a series of rotation matrices. We could use one which
1309 # would be faster but also might cause propagation of error
1310 # matrices = []
1311 # for i in range(numV):
1312 # matrices.append(Matrix.Rotation((rads * i) + rotRange[0], 3, axis))
1313 matrices = [Matrix.Rotation((rads * i) + rotRange[0], 3, axis) for i in range(numV)]
1315 # New vertice coordinates:
1316 verts_out = []
1318 # If two edges were selected:
1319 # - If the lines are not parallel, then it will create a cone-like shaft
1320 if self.shaftType == 0:
1321 for i in range(len(verts) - 2):
1322 init_vec = distance_point_line(verts[i + 2].co, verts[0].co, verts[1].co)
1323 co = init_vec + verts[i + 2].co
1324 # These will be rotated about the origin so will need to be shifted:
1325 for j in range(numV):
1326 verts_out.append(co - (matrices[j] @ init_vec))
1327 elif self.shaftType == 1:
1328 for i in verts:
1329 init_vec = distance_point_line(i.co, active.verts[0].co, active.verts[1].co)
1330 co = init_vec + i.co
1331 # These will be rotated about the origin so will need to be shifted:
1332 for j in range(numV):
1333 verts_out.append(co - (matrices[j] @ init_vec))
1334 # Else if a line and a point was selected:
1335 elif self.shaftType == 2:
1336 init_vec = distance_point_line(verts[2].co, verts[0].co, verts[1].co)
1337 # These will be rotated about the origin so will need to be shifted:
1338 verts_out = [
1339 (verts[i].co - (matrices[j] @ init_vec)) for i in range(2) for j in range(numV)
1341 else:
1342 # Else the above are not possible, so we will just use the edge:
1343 # - The vector defined by the edge is the normal of the plane for the shaft
1344 # - The shaft will have radius "radius"
1345 if is_axial(verts[0].co, verts[1].co) is None:
1346 proj = (verts[1].co - verts[0].co)
1347 proj[2] = 0
1348 norm = proj.cross(verts[1].co - verts[0].co)
1349 vec = norm.cross(verts[1].co - verts[0].co)
1350 vec.length = self.radius
1351 elif is_axial(verts[0].co, verts[1].co) == 'Z':
1352 vec = verts[0].co + Vector((0, 0, self.radius))
1353 else:
1354 vec = verts[0].co + Vector((0, self.radius, 0))
1355 init_vec = distance_point_line(vec, verts[0].co, verts[1].co)
1356 # These will be rotated about the origin so will need to be shifted:
1357 verts_out = [
1358 (verts[i].co - (matrices[j] @ init_vec)) for i in range(2) for j in range(numV)
1361 # We should have the coordinates for a bunch of new verts
1362 # Now add the verts and build the edges and then the faces
1364 newVerts = []
1366 if self.shaftType == 1:
1367 # Vertices:
1368 for i in range(numV * len(verts)):
1369 new = bVerts.new()
1370 new.co = verts_out[i]
1371 bVerts.ensure_lookup_table()
1372 new.select = True
1373 newVerts.append(new)
1374 # Edges:
1375 for i in range(numE):
1376 for j in range(len(verts)):
1377 e = bEdges.new((newVerts[i + (numV * j)], newVerts[i + (numV * j) + 1]))
1378 bEdges.ensure_lookup_table()
1379 e.select = True
1380 for i in range(numV):
1381 for j in range(len(verts) - 1):
1382 e = bEdges.new((newVerts[i + (numV * j)], newVerts[i + (numV * (j + 1))]))
1383 bEdges.ensure_lookup_table()
1384 e.select = True
1386 # Faces: There is a problem with this right now
1388 for i in range(len(edges)):
1389 for j in range(numE):
1390 f = bFaces.new((newVerts[i], newVerts[i + 1],
1391 newVerts[i + (numV * j) + 1], newVerts[i + (numV * j)]))
1392 f.normal_update()
1394 else:
1395 # Vertices:
1396 for i in range(numV * 2):
1397 new = bVerts.new()
1398 new.co = verts_out[i]
1399 new.select = True
1400 bVerts.ensure_lookup_table()
1401 newVerts.append(new)
1402 # Edges:
1403 for i in range(numE):
1404 e = bEdges.new((newVerts[i], newVerts[i + 1]))
1405 e.select = True
1406 bEdges.ensure_lookup_table()
1407 e = bEdges.new((newVerts[i + numV], newVerts[i + numV + 1]))
1408 e.select = True
1409 bEdges.ensure_lookup_table()
1410 for i in range(numV):
1411 e = bEdges.new((newVerts[i], newVerts[i + numV]))
1412 e.select = True
1413 bEdges.ensure_lookup_table()
1414 # Faces:
1415 for i in range(numE):
1416 f = bFaces.new((newVerts[i], newVerts[i + 1],
1417 newVerts[i + numV + 1], newVerts[i + numV]))
1418 bFaces.ensure_lookup_table()
1419 f.normal_update()
1421 bmesh.update_edit_mesh(me)
1423 except Exception as e:
1424 error_handlers(self, "mesh.edgetools_shaft", e,
1425 reports="Shaft Operator failed", func=False)
1426 return {'CANCELLED'}
1428 return {'FINISHED'}
1431 # "Slices" edges crossing a plane defined by a face
1433 class Slice(Operator):
1434 bl_idname = "mesh.edgetools_slice"
1435 bl_label = "Slice"
1436 bl_description = "Cut edges at the plane defined by a selected face"
1437 bl_options = {'REGISTER', 'UNDO'}
1439 make_copy: BoolProperty(
1440 name="Make Copy",
1441 description="Make new vertices at intersection points instead of splitting the edge",
1442 default=False
1444 rip: BoolProperty(
1445 name="Rip",
1446 description="Split into two edges that DO NOT share an intersection vertex",
1447 default=True
1449 pos: BoolProperty(
1450 name="Positive",
1451 description="Remove the portion on the side of the face normal",
1452 default=False
1454 neg: BoolProperty(
1455 name="Negative",
1456 description="Remove the portion on the side opposite of the face normal",
1457 default=False
1460 def draw(self, context):
1461 layout = self.layout
1463 layout.prop(self, "make_copy")
1464 if not self.make_copy:
1465 layout.prop(self, "rip")
1466 layout.label(text="Remove Side:")
1467 layout.prop(self, "pos")
1468 layout.prop(self, "neg")
1470 @classmethod
1471 def poll(cls, context):
1472 ob = context.active_object
1473 return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
1475 def invoke(self, context, event):
1476 return self.execute(context)
1478 def execute(self, context):
1479 try:
1480 me = context.object.data
1481 bm = bmesh.from_edit_mesh(me)
1482 bm.normal_update()
1484 bVerts = bm.verts
1485 bEdges = bm.edges
1486 bFaces = bm.faces
1488 face, normal = None, None
1490 # Find the selected face. This will provide the plane to project onto:
1491 # - First check to use the active face. Allows users to just
1492 # select a bunch of faces with the last being the cutting plane
1493 # - If that fails, then use the first found selected face in the BMesh face list
1494 if isinstance(bm.select_history.active, bmesh.types.BMFace):
1495 face = bm.select_history.active
1496 normal = bm.select_history.active.normal
1497 bm.select_history.active.select = False
1498 else:
1499 for f in bFaces:
1500 if f.select:
1501 face = f
1502 normal = f.normal
1503 f.select = False
1504 break
1506 # If we don't find a selected face exit:
1507 if face is None:
1508 flip_edit_mode()
1509 self.report({'WARNING'},
1510 "Please select a face as the cutting plane. Operation Cancelled")
1511 return {'CANCELLED'}
1513 # Warn the user if they are using an n-gon might lead to some odd results
1514 elif len(face.verts) > 4 and not is_face_planar(face):
1515 self.report({'WARNING'},
1516 "Selected face is an N-gon. Results may be unpredictable")
1518 if ENABLE_DEBUG:
1519 dbg = 0
1520 print("Number of Edges: ", len(bEdges))
1522 for e in bEdges:
1523 if ENABLE_DEBUG:
1524 print("Looping through Edges - ", dbg)
1525 dbg = dbg + 1
1527 # Get the end verts on the edge:
1528 v1 = e.verts[0]
1529 v2 = e.verts[1]
1531 # Make sure that verts are not a part of the cutting plane:
1532 if e.select and (v1 not in face.verts and v2 not in face.verts):
1533 if len(face.verts) < 5: # Not an n-gon
1534 intersection = intersect_line_face(e, face, True)
1535 else:
1536 intersection = intersect_line_plane(v1.co, v2.co, face.verts[0].co, normal)
1538 if ENABLE_DEBUG:
1539 print("Intersection: ", intersection)
1541 # If an intersection exists find the distance of each of the end
1542 # points from the plane, with "positive" being in the direction
1543 # of the cutting plane's normal. If the points are on opposite
1544 # side of the plane, then it intersects and we need to cut it
1545 if intersection is not None:
1546 bVerts.ensure_lookup_table()
1547 bEdges.ensure_lookup_table()
1548 bFaces.ensure_lookup_table()
1550 d1 = distance_point_to_plane(v1.co, face.verts[0].co, normal)
1551 d2 = distance_point_to_plane(v2.co, face.verts[0].co, normal)
1552 # If they have different signs, then the edge crosses the cutting plane:
1553 if abs(d1 + d2) < abs(d1 - d2):
1554 # Make the first vertex the positive one:
1555 if d1 < d2:
1556 v2, v1 = v1, v2
1558 if self.make_copy:
1559 new = bVerts.new()
1560 new.co = intersection
1561 new.select = True
1562 bVerts.ensure_lookup_table()
1563 elif self.rip:
1564 if ENABLE_DEBUG:
1565 print("Branch rip engaged")
1566 newV1 = bVerts.new()
1567 newV1.co = intersection
1568 bVerts.ensure_lookup_table()
1569 if ENABLE_DEBUG:
1570 print("newV1 created", end='; ')
1572 newV2 = bVerts.new()
1573 newV2.co = intersection
1574 bVerts.ensure_lookup_table()
1576 if ENABLE_DEBUG:
1577 print("newV2 created", end='; ')
1579 newE1 = bEdges.new((v1, newV1))
1580 newE2 = bEdges.new((v2, newV2))
1581 bEdges.ensure_lookup_table()
1583 if ENABLE_DEBUG:
1584 print("new edges created", end='; ')
1586 if e.is_valid:
1587 bEdges.remove(e)
1589 bEdges.ensure_lookup_table()
1591 if ENABLE_DEBUG:
1592 print("Old edge removed.\nWe're done with this edge")
1593 else:
1594 new = list(bmesh.utils.edge_split(e, v1, 0.5))
1595 bEdges.ensure_lookup_table()
1596 new[1].co = intersection
1597 e.select = False
1598 new[0].select = False
1599 if self.pos:
1600 bEdges.remove(new[0])
1601 if self.neg:
1602 bEdges.remove(e)
1603 bEdges.ensure_lookup_table()
1605 if ENABLE_DEBUG:
1606 print("The Edge Loop has exited. Now to update the bmesh")
1607 dbg = 0
1609 bmesh.update_edit_mesh(me)
1611 except Exception as e:
1612 error_handlers(self, "mesh.edgetools_slice", e,
1613 reports="Slice Operator failed", func=False)
1614 return {'CANCELLED'}
1616 return {'FINISHED'}
1619 # This projects the selected edges onto the selected plane
1620 # and/or both points on the selected edge
1622 class Project(Operator):
1623 bl_idname = "mesh.edgetools_project"
1624 bl_label = "Project"
1625 bl_description = ("Projects the selected Vertices/Edges onto a selected plane\n"
1626 "(Active is projected onto the rest)")
1627 bl_options = {'REGISTER', 'UNDO'}
1629 make_copy: BoolProperty(
1630 name="Make Copy",
1631 description="Make duplicates of the vertices instead of altering them",
1632 default=False
1635 def draw(self, context):
1636 layout = self.layout
1637 layout.prop(self, "make_copy")
1639 @classmethod
1640 def poll(cls, context):
1641 ob = context.active_object
1642 return (ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
1644 def invoke(self, context, event):
1645 return self.execute(context)
1647 def execute(self, context):
1648 try:
1649 me = context.object.data
1650 bm = bmesh.from_edit_mesh(me)
1651 bm.normal_update()
1653 bFaces = bm.faces
1654 bVerts = bm.verts
1656 fVerts = []
1658 # Find the selected face. This will provide the plane to project onto:
1659 # @todo Check first for an active face
1660 for f in bFaces:
1661 if f.select:
1662 for v in f.verts:
1663 fVerts.append(v)
1664 normal = f.normal
1665 f.select = False
1666 break
1668 for v in bVerts:
1669 if v.select:
1670 if v in fVerts:
1671 v.select = False
1672 continue
1673 d = distance_point_to_plane(v.co, fVerts[0].co, normal)
1674 if self.make_copy:
1675 temp = v
1676 v = bVerts.new()
1677 v.co = temp.co
1678 bVerts.ensure_lookup_table()
1679 vector = normal
1680 vector.length = abs(d)
1681 v.co = v.co - (vector * sign(d))
1682 v.select = False
1684 bmesh.update_edit_mesh(me)
1686 except Exception as e:
1687 error_handlers(self, "mesh.edgetools_project", e,
1688 reports="Project Operator failed", func=False)
1690 return {'CANCELLED'}
1692 return {'FINISHED'}
1695 # Project_End is for projecting/extending an edge to meet a plane
1696 # This is used be selecting a face to define the plane then all the edges
1697 # Then move the vertices in the edge that is closest to the
1698 # plane to the coordinates of the intersection of the edge and the plane
1700 class Project_End(Operator):
1701 bl_idname = "mesh.edgetools_project_end"
1702 bl_label = "Project (End Point)"
1703 bl_description = ("Projects the vertices of the selected\n"
1704 "edges closest to a plane onto that plane")
1705 bl_options = {'REGISTER', 'UNDO'}
1707 make_copy: BoolProperty(
1708 name="Make Copy",
1709 description="Make a duplicate of the vertice instead of moving it",
1710 default=False
1712 keep_length: BoolProperty(
1713 name="Keep Edge Length",
1714 description="Maintain edge lengths",
1715 default=False
1717 use_force: BoolProperty(
1718 name="Use opposite vertices",
1719 description="Force the usage of the vertices at the other end of the edge",
1720 default=False
1722 use_normal: BoolProperty(
1723 name="Project along normal",
1724 description="Use the plane's normal as the projection direction",
1725 default=False
1728 def draw(self, context):
1729 layout = self.layout
1731 if not self.keep_length:
1732 layout.prop(self, "use_normal")
1733 layout.prop(self, "make_copy")
1734 layout.prop(self, "use_force")
1736 @classmethod
1737 def poll(cls, context):
1738 ob = context.active_object
1739 return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
1741 def invoke(self, context, event):
1742 return self.execute(context)
1744 def execute(self, context):
1745 try:
1746 me = context.object.data
1747 bm = bmesh.from_edit_mesh(me)
1748 bm.normal_update()
1750 bFaces = bm.faces
1751 bEdges = bm.edges
1752 bVerts = bm.verts
1754 fVerts = []
1756 # Find the selected face. This will provide the plane to project onto:
1757 for f in bFaces:
1758 if f.select:
1759 for v in f.verts:
1760 fVerts.append(v)
1761 normal = f.normal
1762 f.select = False
1763 break
1765 for e in bEdges:
1766 if e.select:
1767 v1 = e.verts[0]
1768 v2 = e.verts[1]
1769 if v1 in fVerts or v2 in fVerts:
1770 e.select = False
1771 continue
1772 intersection = intersect_line_plane(v1.co, v2.co, fVerts[0].co, normal)
1773 if intersection is not None:
1774 # Use abs because we don't care what side of plane we're on:
1775 d1 = distance_point_to_plane(v1.co, fVerts[0].co, normal)
1776 d2 = distance_point_to_plane(v2.co, fVerts[0].co, normal)
1777 # If d1 is closer than we use v1 as our vertice:
1778 # "xor" with 'use_force':
1779 if (abs(d1) < abs(d2)) is not self.use_force:
1780 if self.make_copy:
1781 v1 = bVerts.new()
1782 v1.co = e.verts[0].co
1783 bVerts.ensure_lookup_table()
1784 bEdges.ensure_lookup_table()
1785 if self.keep_length:
1786 v1.co = intersection
1787 elif self.use_normal:
1788 vector = normal
1789 vector.length = abs(d1)
1790 v1.co = v1.co - (vector * sign(d1))
1791 else:
1792 v1.co = intersection
1793 else:
1794 if self.make_copy:
1795 v2 = bVerts.new()
1796 v2.co = e.verts[1].co
1797 bVerts.ensure_lookup_table()
1798 bEdges.ensure_lookup_table()
1799 if self.keep_length:
1800 v2.co = intersection
1801 elif self.use_normal:
1802 vector = normal
1803 vector.length = abs(d2)
1804 v2.co = v2.co - (vector * sign(d2))
1805 else:
1806 v2.co = intersection
1807 e.select = False
1809 bmesh.update_edit_mesh(me)
1811 except Exception as e:
1812 error_handlers(self, "mesh.edgetools_project_end", e,
1813 reports="Project (End Point) Operator failed", func=False)
1814 return {'CANCELLED'}
1816 return {'FINISHED'}
1819 class VIEW3D_MT_edit_mesh_edgetools(Menu):
1820 bl_label = "Edge Tools"
1821 bl_description = "Various tools for manipulating edges"
1823 def draw(self, context):
1824 layout = self.layout
1826 layout.operator("mesh.edgetools_extend")
1827 layout.operator("mesh.edgetools_spline")
1828 layout.operator("mesh.edgetools_ortho")
1829 layout.operator("mesh.edgetools_shaft")
1830 layout.operator("mesh.edgetools_slice")
1831 layout.separator()
1833 layout.operator("mesh.edgetools_project")
1834 layout.operator("mesh.edgetools_project_end")
1836 def menu_func(self, context):
1837 self.layout.menu("VIEW3D_MT_edit_mesh_edgetools")
1839 # define classes for registration
1840 classes = (
1841 VIEW3D_MT_edit_mesh_edgetools,
1842 Extend,
1843 Spline,
1844 Ortho,
1845 Shaft,
1846 Slice,
1847 Project,
1848 Project_End,
1852 # registering and menu integration
1853 def register():
1854 for cls in classes:
1855 bpy.utils.register_class(cls)
1856 bpy.types.VIEW3D_MT_edit_mesh_context_menu.prepend(menu_func)
1858 # unregistering and removing menus
1859 def unregister():
1860 for cls in classes:
1861 bpy.utils.unregister_class(cls)
1862 bpy.types.VIEW3D_MT_edit_mesh_context_menu.remove(menu_func)
1864 if __name__ == "__main__":
1865 register()