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.
9 "author": "Paul Marshall",
11 "blender": (2, 80, 0),
12 "location": "View3D > Toolbar and View3D > Specials (W-key)",
14 "description": "CAD style edge manipulation tools",
15 "doc_url": "https://wiki.blender.org/index.php/Extensions:2.6/Py/"
16 "Scripts/Modeling/EdgeTools",
23 from bpy
.types
import (
27 from math
import acos
, pi
, radians
, sqrt
28 from mathutils
import Matrix
, Vector
29 from mathutils
.geometry
import (
30 distance_point_to_plane
,
36 from bpy
.props
import (
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
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.
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
84 # Set to True to have the debug prints available
88 # Quick an dirty method for getting the sign of a number:
90 return (number
> 0) - (number
< 0)
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
)
101 # Handle error notifications
102 def error_handlers(self
, op_name
, error
, reports
="ERROR", func
=False):
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"):
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
)
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
)
138 except Exception as e
:
139 error_handlers(self
, "is_selected_enough", e
,
140 "No appropriate selection. Operation Cancelled", func
=True)
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):
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
:
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
):
170 for co1
, co2
in zip(v1
, v2
):
176 def is_face_planar(face
, error
=0.0005):
178 d
= distance_point_to_plane(v
.co
, face
.verts
[0].co
, face
.normal
)
180 print("Distance: " + str(d
))
181 if d
< -error
or d
> error
:
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):
196 print(edge
, end
=", ")
197 print(edges
, end
=", ")
198 print(direction
, end
="; ")
200 # Robustness check: direction cannot be zero
205 for e
in edge
.verts
[0].link_edges
:
206 if e
.select
and edges
.count(e
) == 0:
209 newList
.extend(order_joined_edges(e
, edges
, direction
+ 1))
210 newList
.extend(edges
)
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:
223 newList
.extend(order_joined_edges(e
, edges
, direction
+ 2))
224 newList
.extend(edges
)
227 newList
.extend(edges
)
228 newList
.extend(order_joined_edges(e
, edges
, direction
))
231 print(newList
, end
=", ")
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
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):
268 fraction
= 1 / segments
270 # Form: p1, tangent 1, p2, tangent 2
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]]
291 # Generate each point:
292 for i
in range(segments
- 1):
293 t
= fraction
* (i
+ 1)
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
))
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:
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│
339 Now, the equation of our line can be likewise defined:
342 │y3│ = │a32│ + t3│b32│
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:
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│
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.
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.
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):
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]
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
]
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
:
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
)):
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]
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]
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.
521 # The line is parallel to the z-axis:
523 t12
= ((a11
- a31
) + (b11
- a11
) * t
) / ((a21
- a11
) + (a11
- a21
- b11
+ b21
) * t
)
524 # The line is parallel to the y-axis:
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:
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
))
534 # The line is parallel to the x-axis:
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:
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:
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.
552 t3
= (-a11
+ a31
+ (a11
- b11
) * t
+ (a11
- a21
) *
553 t12
+ (a21
- a11
+ b11
- b21
) * t
* t12
) / (a31
- b31
)
555 t3
= (-a12
+ a32
+ (a12
- b12
) * t
+ (a12
- a22
) *
556 t12
+ (a22
- a12
+ b12
- b22
) * t
* t12
) / (a32
- b32
)
558 t3
= (-a13
+ a33
+ (a13
- b13
) * t
+ (a13
- a23
) *
559 t12
+ (a23
- a13
+ b13
- b23
) * t
* t12
) / (a33
- b33
)
562 print("The second edge is a zero-length edge")
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
))
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
:
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
:
588 # These must be unit vectors, else we risk a domain error:
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
)):
601 # This is the default case where we either have a planar quad or an n-gon
603 int_co
= intersect_line_plane(edge
.verts
[0].co
, edge
.verts
[1].co
,
604 face
.verts
[0].co
, face
.normal
)
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
):
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
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"
639 bl_description
= "Extend the selected edges of vertex pairs"
640 bl_options
= {'REGISTER', 'UNDO'}
644 description
="Extend the edge forwards",
649 description
="Extend the edge backwards",
652 length
: FloatProperty(
654 description
="Length to extend the edge",
659 def draw(self
, context
):
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")
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
):
678 me
= context
.object.data
679 bm
= bmesh
.from_edit_mesh(me
)
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"):
693 vector
= e
.verts
[0].co
- e
.verts
[1].co
694 vector
.length
= self
.length
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()
703 v
.co
= e
.verts
[0].co
+ vector
704 newE
= bEdges
.new((e
.verts
[0], v
))
705 bEdges
.ensure_lookup_table()
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()
713 v
.co
= e
.verts
[1].co
- vector
714 newE
= bEdges
.new((e
.verts
[1], v
))
715 bEdges
.ensure_lookup_table()
717 vector
= verts
[0].co
- verts
[1].co
718 vector
.length
= self
.length
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()
727 v
.co
= verts
[0].co
+ vector
728 e
= bEdges
.new((verts
[0], v
))
729 bEdges
.ensure_lookup_table()
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()
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)
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"
761 bl_description
= "Create a spline interplopation between two edges"
762 bl_options
= {'REGISTER', 'UNDO'}
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")],
772 segments
: IntProperty(
774 description
="Number of segments to use in the interpolation",
781 description
="Flip the direction of the spline on Edge 1",
786 description
="Flip the direction of the spline on Edge 2",
791 description
="Tension on Edge 1",
792 min=-4096.0, max=4096.0,
793 soft_min
=-8.0, soft_max
=8.0,
798 description
="Tension on Edge 2",
799 min=-4096.0, max=4096.0,
800 soft_min
=-8.0, soft_max
=8.0,
804 def draw(self
, context
):
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)
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
):
830 me
= context
.object.data
831 bm
= bmesh
.from_edit_mesh(me
)
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"):
843 verts
= [edges
[v
// 2].verts
[v
% 2] for v
in range(4)]
848 p1_dir
= verts
[1].co
- verts
[0].co
852 p1_dir
= verts
[0].co
- verts
[1].co
855 p1_dir
.length
= -self
.ten1
857 p1_dir
.length
= self
.ten1
862 p2_dir
= verts
[2].co
- verts
[3].co
866 p2_dir
= verts
[3].co
- verts
[2].co
869 p2_dir
.length
= -self
.ten2
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'
893 # Add vertices and set the points:
894 for i
in range(seg
- 1):
897 bVerts
.ensure_lookup_table()
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)
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
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'}
937 description
="Enable edge creation for Vertice 1",
942 description
="Enable edge creation for Vertice 2",
947 description
="Enable edge creation for Vertice 3",
952 description
="Enable edge creation for Vertice 4",
957 description
="Enable creation of positive direction edges",
962 description
="Enable creation of negative direction edges",
965 angle
: FloatProperty(
967 description
="Define the angle off of the originating edge",
971 length
: FloatProperty(
973 description
="Length of created edges",
977 # For when only one edge is selected (Possible feature to be testd):
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")],
986 def draw(self
, context
):
989 layout
.label(text
="Creation:")
990 split
= layout
.split()
993 col
.prop(self
, "vert1", toggle
=True)
994 col
.prop(self
, "vert2", toggle
=True)
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")
1008 col
= layout
.column(align
=True)
1009 col
.prop(self
, "angle")
1010 col
.prop(self
, "length")
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
):
1022 me
= context
.object.data
1023 bm
= bmesh
.from_edit_mesh(me
)
1028 edges
= [e
for e
in bEdges
if e
.select
]
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],
1039 cos
= intersect_line_line(verts
[0].co
, verts
[1].co
, verts
[2].co
, verts
[3].co
)
1041 # If the two edges are parallel:
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])
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]))
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.
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
)):
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
)):
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()
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'}
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"
1127 bl_description
= "Create a shaft mesh around an axis"
1128 bl_options
= {'REGISTER', 'UNDO'}
1130 # Selection defaults:
1133 # For tracking if the user has changed selection:
1134 last_edge
: IntProperty(
1136 description
="Tracks if user has changed selected edges",
1144 description
="Edge to shaft around",
1149 name
="Flip Second Edge",
1150 description
="Flip the perceived direction of the second edge",
1153 radius
: FloatProperty(
1155 description
="Shaft Radius",
1156 min=0.0, max=1024.0,
1159 start
: FloatProperty(
1160 name
="Starting Angle",
1161 description
="Angle to start the shaft at",
1162 min=-360.0, max=360.0,
1165 finish
: FloatProperty(
1166 name
="Ending Angle",
1167 description
="Angle to end the shaft at",
1168 min=-360.0, max=360.0,
1171 segments
: IntProperty(
1172 name
="Shaft Segments",
1173 description
="Number of segments to use in the shaft",
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")
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:
1202 return self
.execute(context
)
1204 def execute(self
, context
):
1206 me
= context
.object.data
1207 bm
= bmesh
.from_edit_mesh(me
)
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:
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]]
1252 self
.report({'WARNING'},
1253 "Active geometry is not an edge. Operation Cancelled")
1254 return {'CANCELLED'}
1255 elif self
.edge
== 1:
1258 verts
.append(edges
[edge
[0]].verts
[0])
1259 verts
.append(edges
[edge
[0]].verts
[1])
1264 verts
.append(edges
[edge
[1]].verts
[vert
[0]])
1265 verts
.append(edges
[edge
[1]].verts
[vert
[1]])
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])
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])
1285 self
.report({'WARNING'},
1286 "Active geometry is not an edge. Operation Cancelled")
1287 return {'CANCELLED'}
1290 verts
.append(edges
[0].verts
[0])
1291 verts
.append(edges
[0].verts
[1])
1294 if v
.select
and verts
.count(v
) == 0:
1302 # The vector denoting the axis of rotation:
1303 if self
.shaftType
== 1:
1304 axis
= active
.verts
[1].co
- active
.verts
[0].co
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
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:
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:
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:
1339 (verts
[i
].co
- (matrices
[j
] @ init_vec
)) for i
in range(2) for j
in range(numV
)
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
)
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
))
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:
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
1366 if self
.shaftType
== 1:
1368 for i
in range(numV
* len(verts
)):
1370 new
.co
= verts_out
[i
]
1371 bVerts
.ensure_lookup_table()
1373 newVerts
.append(new
)
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()
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()
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)]))
1396 for i
in range(numV
* 2):
1398 new
.co
= verts_out
[i
]
1400 bVerts
.ensure_lookup_table()
1401 newVerts
.append(new
)
1403 for i
in range(numE
):
1404 e
= bEdges
.new((newVerts
[i
], newVerts
[i
+ 1]))
1406 bEdges
.ensure_lookup_table()
1407 e
= bEdges
.new((newVerts
[i
+ numV
], newVerts
[i
+ numV
+ 1]))
1409 bEdges
.ensure_lookup_table()
1410 for i
in range(numV
):
1411 e
= bEdges
.new((newVerts
[i
], newVerts
[i
+ numV
]))
1413 bEdges
.ensure_lookup_table()
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()
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'}
1431 # "Slices" edges crossing a plane defined by a face
1433 class Slice(Operator
):
1434 bl_idname
= "mesh.edgetools_slice"
1436 bl_description
= "Cut edges at the plane defined by a selected face"
1437 bl_options
= {'REGISTER', 'UNDO'}
1439 make_copy
: BoolProperty(
1441 description
="Make new vertices at intersection points instead of splitting the edge",
1446 description
="Split into two edges that DO NOT share an intersection vertex",
1451 description
="Remove the portion on the side of the face normal",
1456 description
="Remove the portion on the side opposite of the face normal",
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")
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
):
1480 me
= context
.object.data
1481 bm
= bmesh
.from_edit_mesh(me
)
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
1506 # If we don't find a selected face exit:
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")
1520 print("Number of Edges: ", len(bEdges
))
1524 print("Looping through Edges - ", dbg
)
1527 # Get the end verts on the edge:
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)
1536 intersection
= intersect_line_plane(v1
.co
, v2
.co
, face
.verts
[0].co
, normal
)
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:
1560 new
.co
= intersection
1562 bVerts
.ensure_lookup_table()
1565 print("Branch rip engaged")
1566 newV1
= bVerts
.new()
1567 newV1
.co
= intersection
1568 bVerts
.ensure_lookup_table()
1570 print("newV1 created", end
='; ')
1572 newV2
= bVerts
.new()
1573 newV2
.co
= intersection
1574 bVerts
.ensure_lookup_table()
1577 print("newV2 created", end
='; ')
1579 newE1
= bEdges
.new((v1
, newV1
))
1580 newE2
= bEdges
.new((v2
, newV2
))
1581 bEdges
.ensure_lookup_table()
1584 print("new edges created", end
='; ')
1589 bEdges
.ensure_lookup_table()
1592 print("Old edge removed.\nWe're done with this edge")
1594 new
= list(bmesh
.utils
.edge_split(e
, v1
, 0.5))
1595 bEdges
.ensure_lookup_table()
1596 new
[1].co
= intersection
1598 new
[0].select
= False
1600 bEdges
.remove(new
[0])
1603 bEdges
.ensure_lookup_table()
1606 print("The Edge Loop has exited. Now to update the bmesh")
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'}
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(
1631 description
="Make duplicates of the vertices instead of altering them",
1635 def draw(self
, context
):
1636 layout
= self
.layout
1637 layout
.prop(self
, "make_copy")
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
):
1649 me
= context
.object.data
1650 bm
= bmesh
.from_edit_mesh(me
)
1658 # Find the selected face. This will provide the plane to project onto:
1659 # @todo Check first for an active face
1673 d
= distance_point_to_plane(v
.co
, fVerts
[0].co
, normal
)
1678 bVerts
.ensure_lookup_table()
1680 vector
.length
= abs(d
)
1681 v
.co
= v
.co
- (vector
* sign(d
))
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'}
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(
1709 description
="Make a duplicate of the vertice instead of moving it",
1712 keep_length
: BoolProperty(
1713 name
="Keep Edge Length",
1714 description
="Maintain edge lengths",
1717 use_force
: BoolProperty(
1718 name
="Use opposite vertices",
1719 description
="Force the usage of the vertices at the other end of the edge",
1722 use_normal
: BoolProperty(
1723 name
="Project along normal",
1724 description
="Use the plane's normal as the projection direction",
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")
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
):
1746 me
= context
.object.data
1747 bm
= bmesh
.from_edit_mesh(me
)
1756 # Find the selected face. This will provide the plane to project onto:
1769 if v1
in fVerts
or v2
in fVerts
:
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
:
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
:
1789 vector
.length
= abs(d1
)
1790 v1
.co
= v1
.co
- (vector
* sign(d1
))
1792 v1
.co
= intersection
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
:
1803 vector
.length
= abs(d2
)
1804 v2
.co
= v2
.co
- (vector
* sign(d2
))
1806 v2
.co
= intersection
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'}
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")
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
1841 VIEW3D_MT_edit_mesh_edgetools
,
1852 # registering and menu integration
1855 bpy
.utils
.register_class(cls
)
1856 bpy
.types
.VIEW3D_MT_edit_mesh_context_menu
.prepend(menu_func
)
1858 # unregistering and removing menus
1861 bpy
.utils
.unregister_class(cls
)
1862 bpy
.types
.VIEW3D_MT_edit_mesh_context_menu
.remove(menu_func
)
1864 if __name__
== "__main__":