1 # The Blender Edgetools is to bring CAD tools to Blender.
2 # Copyright (C) 2012 Paul Marshall
4 # ##### BEGIN GPL LICENSE BLOCK #####
6 # This program is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation, either version 3 of the License, or
9 # (at your option) any later version.
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
19 # ##### END GPL LICENSE BLOCK #####
25 "author": "Paul Marshall",
27 "blender": (2, 68, 0),
28 "location": "View3D > Toolbar and View3D > Specials (W-key)",
30 "description": "CAD style edge manipulation tools",
31 "wiki_url": "https://wiki.blender.org/index.php/Extensions:2.6/Py/"
32 "Scripts/Modeling/EdgeTools",
38 from bpy
.types
import (
42 from math
import acos
, pi
, radians
, sqrt
43 from mathutils
import Matrix
, Vector
44 from mathutils
.geometry
import (
45 distance_point_to_plane
,
51 from bpy
.props
import (
60 This is a toolkit for edge manipulation based on mesh manipulation
61 abilities of several CAD/CAE packages, notably CATIA's Geometric Workbench
62 from which most of these tools have a functional basis.
64 The GUI and Blender add-on structure shamelessly coded in imitation of the
68 - "Ortho" inspired from CATIA's line creation tool which creates a line of a
69 user specified length at a user specified angle to a curve at a chosen
70 point. The user then selects the plane the line is to be created in.
71 - "Shaft" is inspired from CATIA's tool of the same name. However, instead
72 of a curve around an axis, this will instead shaft a line, a point, or
73 a fixed radius about the selected axis.
74 - "Slice" is from CATIA's ability to split a curve on a plane. When
75 completed this be a Python equivalent with all the same basic
76 functionality, though it will sadly be a little clumsier to use due
77 to Blender's selection limitations.
80 - Fillet operator and related functions removed as they didn't work
81 - Buggy parts have been hidden behind ENABLE_DEBUG global (set it to True)
82 Example: Shaft with more than two edges selected
84 Paul "BrikBot" Marshall
85 Created: January 28, 2012
86 Last Modified: October 6, 2012
88 Coded in IDLE, tested in Blender 2.6.
89 Search for "@todo" to quickly find sections that need work
91 Note: lijenstina - modified this script in preparation for merging
92 fixed the needless jumping to object mode for bmesh creation
93 causing the crash with the Slice > Rip operator
94 Removed the test operator since version 0.9.2
95 added general error handling
99 # Set to True to have the debug prints available
103 # Quick an dirty method for getting the sign of a number:
105 return (number
> 0) - (number
< 0)
109 # Checks to see if two lines are parallel
111 def is_parallel(v1
, v2
, v3
, v4
):
112 result
= intersect_line_line(v1
, v2
, v3
, v4
)
113 return result
is None
116 # Handle error notifications
117 def error_handlers(self
, op_name
, error
, reports
="ERROR", func
=False):
119 self
.report({'WARNING'}, reports
+ " (See Console for more info)")
121 is_func
= "Function" if func
else "Operator"
122 print("\n[Mesh EdgeTools]\n{}: {}\nError: {}\n".format(is_func
, op_name
, error
))
125 def flip_edit_mode():
126 bpy
.ops
.object.editmode_toggle()
127 bpy
.ops
.object.editmode_toggle()
130 # check the appropriate selection condition
131 # to prevent crashes with the index out of range errors
132 # pass the bEdges and bVerts based selection tables here
133 # types: Edge, Vertex, All
134 def is_selected_enough(self
, bEdges
, bVerts
, edges_n
=1, verts_n
=0, types
="Edge"):
137 if bEdges
and types
== "Edge":
138 check
= (len(bEdges
) >= edges_n
)
139 elif bVerts
and types
== "Vertex":
140 check
= (len(bVerts
) >= verts_n
)
141 elif bEdges
and bVerts
and types
== "All":
142 check
= (len(bEdges
) >= edges_n
and len(bVerts
) >= verts_n
)
145 strings
= "%s Vertices and / or " % verts_n
if verts_n
!= 0 else ""
146 self
.report({'WARNING'},
147 "Needs at least " + strings
+ "%s Edge(s) selected. "
148 "Operation Cancelled" % edges_n
)
153 except Exception as e
:
154 error_handlers(self
, "is_selected_enough", e
,
155 "No appropriate selection. Operation Cancelled", func
=True)
162 # This is for the special case where the edge is parallel to an axis.
163 # The projection onto the XY plane will fail so it will have to be handled differently
165 def is_axial(v1
, v2
, error
=0.000002):
167 # Don't need to store, but is easier to read:
168 vec0
= vector
[0] > -error
and vector
[0] < error
169 vec1
= vector
[1] > -error
and vector
[1] < error
170 vec2
= vector
[2] > -error
and vector
[2] < error
171 if (vec0
or vec1
) and vec2
:
179 # For some reason "Vector = Vector" does not seem to look at the actual coordinates
181 def is_same_co(v1
, v2
):
182 if len(v1
) != len(v2
):
185 for co1
, co2
in zip(v1
, v2
):
191 def is_face_planar(face
, error
=0.0005):
193 d
= distance_point_to_plane(v
.co
, face
.verts
[0].co
, face
.normal
)
195 print("Distance: " + str(d
))
196 if d
< -error
or d
> error
:
202 # Starts with an edge. Then scans for linked, selected edges and builds a
203 # list with them in "order", starting at one end and moving towards the other
205 def order_joined_edges(edge
, edges
=[], direction
=1):
211 print(edge
, end
=", ")
212 print(edges
, end
=", ")
213 print(direction
, end
="; ")
215 # Robustness check: direction cannot be zero
220 for e
in edge
.verts
[0].link_edges
:
221 if e
.select
and edges
.count(e
) == 0:
224 newList
.extend(order_joined_edges(e
, edges
, direction
+ 1))
225 newList
.extend(edges
)
228 newList
.extend(edges
)
229 newList
.extend(order_joined_edges(e
, edges
, direction
- 1))
231 # This will only matter at the first level:
232 direction
= direction
* -1
234 for e
in edge
.verts
[1].link_edges
:
235 if e
.select
and edges
.count(e
) == 0:
238 newList
.extend(order_joined_edges(e
, edges
, direction
+ 2))
239 newList
.extend(edges
)
242 newList
.extend(edges
)
243 newList
.extend(order_joined_edges(e
, edges
, direction
))
246 print(newList
, end
=", ")
252 # --------------- GEOMETRY CALCULATION METHODS --------------
254 # distance_point_line
255 # I don't know why the mathutils.geometry API does not already have this, but
256 # it is trivial to code using the structures already in place. Instead of
257 # returning a float, I also want to know the direction vector defining the
258 # distance. Distance can be found with "Vector.length"
260 def distance_point_line(pt
, line_p1
, line_p2
):
261 int_co
= intersect_point_line(pt
, line_p1
, line_p2
)
262 distance_vector
= int_co
[0] - pt
263 return distance_vector
266 # interpolate_line_line
267 # This is an experiment into a cubic Hermite spline (c-spline) for connecting
268 # two edges with edges that obey the general equation.
269 # This will return a set of point coordinates (Vectors)
271 # A good, easy to read background on the mathematics can be found at:
272 # http://cubic.org/docs/hermite.htm
274 # Right now this is . . . less than functional :P
276 # - C-Spline and Bezier curves do not end on p2_co as they are supposed to.
277 # - B-Spline just fails. Epically.
278 # - Add more methods as I come across them. Who said flexibility was bad?
280 def interpolate_line_line(p1_co
, p1_dir
, p2_co
, p2_dir
, segments
, tension
=1,
281 typ
='BEZIER', include_ends
=False):
283 fraction
= 1 / segments
285 # Form: p1, tangent 1, p2, tangent 2
287 poly
= [[2, -3, 0, 1], [1, -2, 1, 0],
288 [-2, 3, 0, 0], [1, -1, 0, 0]]
289 elif typ
== 'BEZIER':
290 poly
= [[-1, 3, -3, 1], [3, -6, 3, 0],
291 [1, 0, 0, 0], [-3, 3, 0, 0]]
292 p1_dir
= p1_dir
+ p1_co
293 p2_dir
= -p2_dir
+ p2_co
294 elif typ
== 'BSPLINE':
295 # Supposed poly matrix for a cubic b-spline:
296 # poly = [[-1, 3, -3, 1], [3, -6, 3, 0],
297 # [-3, 0, 3, 0], [1, 4, 1, 0]]
298 # My own invention to try to get something that somewhat acts right
299 # This is semi-quadratic rather than fully cubic:
300 poly
= [[0, -1, 0, 1], [1, -2, 1, 0],
301 [0, -1, 2, 0], [1, -1, 0, 0]]
306 # Generate each point:
307 for i
in range(segments
- 1):
308 t
= fraction
* (i
+ 1)
311 s
= [t
** 3, t
** 2, t
, 1]
312 h00
= (poly
[0][0] * s
[0]) + (poly
[0][1] * s
[1]) + (poly
[0][2] * s
[2]) + (poly
[0][3] * s
[3])
313 h01
= (poly
[1][0] * s
[0]) + (poly
[1][1] * s
[1]) + (poly
[1][2] * s
[2]) + (poly
[1][3] * s
[3])
314 h10
= (poly
[2][0] * s
[0]) + (poly
[2][1] * s
[1]) + (poly
[2][2] * s
[2]) + (poly
[2][3] * s
[3])
315 h11
= (poly
[3][0] * s
[0]) + (poly
[3][1] * s
[1]) + (poly
[3][2] * s
[2]) + (poly
[3][3] * s
[3])
316 pieces
.append((h00
* p1_co
) + (h01
* p1_dir
) + (h10
* p2_co
) + (h11
* p2_dir
))
329 # intersect_line_face
331 # Calculates the coordinate of intersection of a line with a face. It returns
332 # the coordinate if one exists, otherwise None. It can only deal with tris or
333 # quads for a face. A quad does NOT have to be planar
335 Quad math and theory:
336 A quad may not be planar. Therefore the treated definition of the surface is
337 that the surface is composed of all lines bridging two other lines defined by
338 the given four points. The lines do not "cross"
340 The two lines in 3-space can defined as:
341 ┌ ┐ ┌ ┐ ┌ ┐ ┌ ┐ ┌ ┐ ┌ ┐
342 │x1│ │a11│ │b11│ │x2│ │a21│ │b21│
343 │y1│ = (1-t1)│a12│ + t1│b12│, │y2│ = (1-t2)│a22│ + t2│b22│
344 │z1│ │a13│ │b13│ │z2│ │a23│ │b23│
345 └ ┘ └ ┘ └ ┘ └ ┘ └ ┘ └ ┘
346 Therefore, the surface is the lines defined by every point alone the two
347 lines with a same "t" value (t1 = t2). This is basically R = V1 + tQ, where
348 Q = V2 - V1 therefore R = V1 + t(V2 - V1) -> R = (1 - t)V1 + tV2:
350 │x12│ │(1-t)a11 + t * b11│ │(1-t)a21 + t * b21│
351 │y12│ = (1 - t12)│(1-t)a12 + t * b12│ + t12│(1-t)a22 + t * b22│
352 │z12│ │(1-t)a13 + t * b13│ │(1-t)a23 + t * b23│
354 Now, the equation of our line can be likewise defined:
357 │y3│ = │a32│ + t3│b32│
360 Now we just have to find a valid solution for the two equations. This should
361 be our point of intersection. Therefore, x12 = x3 -> x, y12 = y3 -> y,
362 z12 = z3 -> z. Thus, to find that point we set the equation defining the
363 surface as equal to the equation for the line:
365 │(1-t)a11 + t * b11│ │(1-t)a21 + t * b21│ │a31│ │b31│
366 (1 - t12)│(1-t)a12 + t * b12│ + t12│(1-t)a22 + t * b22│ = │a32│ + t3│b32│
367 │(1-t)a13 + t * b13│ │(1-t)a23 + t * b23│ │a33│ │b33│
369 This leaves us with three equations, three unknowns. Solving the system by
370 hand is practically impossible, but using Mathematica we are given an insane
371 series of three equations (not reproduced here for the sake of space: see
372 http://www.mediafire.com/file/cc6m6ba3sz2b96m/intersect_line_surface.nb and
373 http://www.mediafire.com/file/0egbr5ahg14talm/intersect_line_surface2.nb for
374 Mathematica computation).
376 Additionally, the resulting series of equations may result in a div by zero
377 exception if the line in question if parallel to one of the axis or if the
378 quad is planar and parallel to either the XY, XZ, or YZ planes. However, the
379 system is still solvable but must be dealt with a little differently to avaid
380 these special cases. Because the resulting equations are a little different,
381 we have to code them differently. 00Hence the special cases.
384 A triangle must be planar (three points define a plane). So we just
385 have to make sure that the line intersects inside the triangle.
387 If the point is within the triangle, then the angle between the lines that
388 connect the point to the each individual point of the triangle will be
389 equal to 2 * PI. Otherwise, if the point is outside the triangle, then the
390 sum of the angles will be less.
393 # - Figure out how to deal with n-gons
394 # How the heck is a face with 8 verts definied mathematically?
395 # How do I then find the intersection point of a line with said vert?
396 # How do I know if that point is "inside" all the verts?
397 # I have no clue, and haven't been able to find anything on it so far
398 # Maybe if someone (actually reads this and) who knows could note?
401 def intersect_line_face(edge
, face
, is_infinite
=False, error
=0.000002):
404 # If we are dealing with a non-planar quad:
405 if len(face
.verts
) == 4 and not is_face_planar(face
):
406 edgeA
= face
.edges
[0]
410 for i
in range(len(face
.edges
)):
411 if face
.edges
[i
].verts
[0] not in edgeA
.verts
and \
412 face
.edges
[i
].verts
[1] not in edgeA
.verts
:
414 edgeB
= face
.edges
[i
]
417 # I haven't figured out a way to mix this in with the above. Doing so might remove a
418 # few extra instructions from having to be executed saving a few clock cycles:
419 for i
in range(len(face
.edges
)):
420 if face
.edges
[i
] == edgeA
or face
.edges
[i
] == edgeB
:
422 if ((edgeA
.verts
[0] in face
.edges
[i
].verts
and
423 edgeB
.verts
[1] in face
.edges
[i
].verts
) or
424 (edgeA
.verts
[1] in face
.edges
[i
].verts
and edgeB
.verts
[0] in face
.edges
[i
].verts
)):
429 # Define calculation coefficient constants:
430 # "xx1" is the x coordinate, "xx2" is the y coordinate, and "xx3" is the z coordinate
431 a11
, a12
, a13
= edgeA
.verts
[0].co
[0], edgeA
.verts
[0].co
[1], edgeA
.verts
[0].co
[2]
432 b11
, b12
, b13
= edgeA
.verts
[1].co
[0], edgeA
.verts
[1].co
[1], edgeA
.verts
[1].co
[2]
435 a21
, a22
, a23
= edgeB
.verts
[1].co
[0], edgeB
.verts
[1].co
[1], edgeB
.verts
[1].co
[2]
436 b21
, b22
, b23
= edgeB
.verts
[0].co
[0], edgeB
.verts
[0].co
[1], edgeB
.verts
[0].co
[2]
438 a21
, a22
, a23
= edgeB
.verts
[0].co
[0], edgeB
.verts
[0].co
[1], edgeB
.verts
[0].co
[2]
439 b21
, b22
, b23
= edgeB
.verts
[1].co
[0], edgeB
.verts
[1].co
[1], edgeB
.verts
[1].co
[2]
440 a31
, a32
, a33
= edge
.verts
[0].co
[0], edge
.verts
[0].co
[1], edge
.verts
[0].co
[2]
441 b31
, b32
, b33
= edge
.verts
[1].co
[0], edge
.verts
[1].co
[1], edge
.verts
[1].co
[2]
443 # There are a bunch of duplicate "sub-calculations" inside the resulting
444 # equations for t, t12, and t3. Calculate them once and store them to
445 # reduce computational time:
446 m01
= a13
* a22
* a31
447 m02
= a12
* a23
* a31
448 m03
= a13
* a21
* a32
449 m04
= a11
* a23
* a32
450 m05
= a12
* a21
* a33
451 m06
= a11
* a22
* a33
452 m07
= a23
* a32
* b11
453 m08
= a22
* a33
* b11
454 m09
= a23
* a31
* b12
455 m10
= a21
* a33
* b12
456 m11
= a22
* a31
* b13
457 m12
= a21
* a32
* b13
458 m13
= a13
* a32
* b21
459 m14
= a12
* a33
* b21
460 m15
= a13
* a31
* b22
461 m16
= a11
* a33
* b22
462 m17
= a12
* a31
* b23
463 m18
= a11
* a32
* b23
464 m19
= a13
* a22
* b31
465 m20
= a12
* a23
* b31
466 m21
= a13
* a32
* b31
467 m22
= a23
* a32
* b31
468 m23
= a12
* a33
* b31
469 m24
= a22
* a33
* b31
470 m25
= a23
* b12
* b31
471 m26
= a33
* b12
* b31
472 m27
= a22
* b13
* b31
473 m28
= a32
* b13
* b31
474 m29
= a13
* b22
* b31
475 m30
= a33
* b22
* b31
476 m31
= a12
* b23
* b31
477 m32
= a32
* b23
* b31
478 m33
= a13
* a21
* b32
479 m34
= a11
* a23
* b32
480 m35
= a13
* a31
* b32
481 m36
= a23
* a31
* b32
482 m37
= a11
* a33
* b32
483 m38
= a21
* a33
* b32
484 m39
= a23
* b11
* b32
485 m40
= a33
* b11
* b32
486 m41
= a21
* b13
* b32
487 m42
= a31
* b13
* b32
488 m43
= a13
* b21
* b32
489 m44
= a33
* b21
* b32
490 m45
= a11
* b23
* b32
491 m46
= a31
* b23
* b32
492 m47
= a12
* a21
* b33
493 m48
= a11
* a22
* b33
494 m49
= a12
* a31
* b33
495 m50
= a22
* a31
* b33
496 m51
= a11
* a32
* b33
497 m52
= a21
* a32
* b33
498 m53
= a22
* b11
* b33
499 m54
= a32
* b11
* b33
500 m55
= a21
* b12
* b33
501 m56
= a31
* b12
* b33
502 m57
= a12
* b21
* b33
503 m58
= a32
* b21
* b33
504 m59
= a11
* b22
* b33
505 m60
= a31
* b22
* b33
506 m61
= a33
* b12
* b21
507 m62
= a32
* b13
* b21
508 m63
= a33
* b11
* b22
509 m64
= a31
* b13
* b22
510 m65
= a32
* b11
* b23
511 m66
= a31
* b12
* b23
512 m67
= b13
* b22
* b31
513 m68
= b12
* b23
* b31
514 m69
= b13
* b21
* b32
515 m70
= b11
* b23
* b32
516 m71
= b12
* b21
* b33
517 m72
= b11
* b22
* b33
518 n01
= m01
- m02
- m03
+ m04
+ m05
- m06
519 n02
= -m07
+ m08
+ m09
- m10
- m11
+ m12
+ m13
- m14
- m15
+ m16
+ m17
- m18
- \
520 m25
+ m27
+ m29
- m31
+ m39
- m41
- m43
+ m45
- m53
+ m55
+ m57
- m59
521 n03
= -m19
+ m20
+ m33
- m34
- m47
+ m48
522 n04
= m21
- m22
- m23
+ m24
- m35
+ m36
+ m37
- m38
+ m49
- m50
- m51
+ m52
523 n05
= m26
- m28
- m30
+ m32
- m40
+ m42
+ m44
- m46
+ m54
- m56
- m58
+ m60
524 n06
= m61
- m62
- m63
+ m64
+ m65
- m66
- m67
+ m68
+ m69
- m70
- m71
+ m72
525 n07
= 2 * n01
+ n02
+ 2 * n03
+ n04
+ n05
526 n08
= n01
+ n02
+ n03
+ n06
528 # Calculate t, t12, and t3:
529 t
= (n07
- sqrt(pow(-n07
, 2) - 4 * (n01
+ n03
+ n04
) * n08
)) / (2 * n08
)
531 # t12 can be greatly simplified by defining it with t in it:
532 # If block used to help prevent any div by zero error.
536 # The line is parallel to the z-axis:
538 t12
= ((a11
- a31
) + (b11
- a11
) * t
) / ((a21
- a11
) + (a11
- a21
- b11
+ b21
) * t
)
539 # The line is parallel to the y-axis:
541 t12
= ((a11
- a31
) + (b11
- a11
) * t
) / ((a21
- a11
) + (a11
- a21
- b11
+ b21
) * t
)
542 # The line is along the y/z-axis but is not parallel to either:
544 t12
= -(-(a33
- b33
) * (-a32
+ a12
* (1 - t
) + b12
* t
) + (a32
- b32
) *
545 (-a33
+ a13
* (1 - t
) + b13
* t
)) / (-(a33
- b33
) *
546 ((a22
- a12
) * (1 - t
) + (b22
- b12
) * t
) + (a32
- b32
) *
547 ((a23
- a13
) * (1 - t
) + (b23
- b13
) * t
))
549 # The line is parallel to the x-axis:
551 t12
= ((a12
- a32
) + (b12
- a12
) * t
) / ((a22
- a12
) + (a12
- a22
- b12
+ b22
) * t
)
552 # The line is along the x/z-axis but is not parallel to either:
554 t12
= -(-(a33
- b33
) * (-a31
+ a11
* (1 - t
) + b11
* t
) + (a31
- b31
) * (-a33
+ a13
*
555 (1 - t
) + b13
* t
)) / (-(a33
- b33
) * ((a21
- a11
) * (1 - t
) + (b21
- b11
) * t
) +
556 (a31
- b31
) * ((a23
- a13
) * (1 - t
) + (b23
- b13
) * t
))
557 # The line is along the x/y-axis but is not parallel to either:
559 t12
= -(-(a32
- b32
) * (-a31
+ a11
* (1 - t
) + b11
* t
) + (a31
- b31
) * (-a32
+ a12
*
560 (1 - t
) + b12
* t
)) / (-(a32
- b32
) * ((a21
- a11
) * (1 - t
) + (b21
- b11
) * t
) +
561 (a31
- b31
) * ((a22
- a21
) * (1 - t
) + (b22
- b12
) * t
))
563 # Likewise, t3 is greatly simplified by defining it in terms of t and t12:
564 # If block used to prevent a div by zero error.
567 t3
= (-a11
+ a31
+ (a11
- b11
) * t
+ (a11
- a21
) *
568 t12
+ (a21
- a11
+ b11
- b21
) * t
* t12
) / (a31
- b31
)
570 t3
= (-a12
+ a32
+ (a12
- b12
) * t
+ (a12
- a22
) *
571 t12
+ (a22
- a12
+ b12
- b22
) * t
* t12
) / (a32
- b32
)
573 t3
= (-a13
+ a33
+ (a13
- b13
) * t
+ (a13
- a23
) *
574 t12
+ (a23
- a13
+ b13
- b23
) * t
* t12
) / (a33
- b33
)
577 print("The second edge is a zero-length edge")
580 # Calculate the point of intersection:
581 x
= (1 - t3
) * a31
+ t3
* b31
582 y
= (1 - t3
) * a32
+ t3
* b32
583 z
= (1 - t3
) * a33
+ t3
* b33
584 int_co
= Vector((x
, y
, z
))
589 # If the line does not intersect the quad, we return "None":
590 if (t
< -1 or t
> 1 or t12
< -1 or t12
> 1) and not is_infinite
:
593 elif len(face
.verts
) == 3:
594 p1
, p2
, p3
= face
.verts
[0].co
, face
.verts
[1].co
, face
.verts
[2].co
595 int_co
= intersect_line_plane(edge
.verts
[0].co
, edge
.verts
[1].co
, p1
, face
.normal
)
597 # Only check if the triangle is not being treated as an infinite plane:
598 # Math based from http://paulbourke.net/geometry/linefacet/
599 if int_co
is not None and not is_infinite
:
603 # These must be unit vectors, else we risk a domain error:
607 aAB
= acos(pA
.dot(pB
))
608 aBC
= acos(pB
.dot(pC
))
609 aCA
= acos(pC
.dot(pA
))
610 sumA
= aAB
+ aBC
+ aCA
612 # If the point is outside the triangle:
613 if (sumA
> (pi
+ error
) and sumA
< (pi
- error
)):
616 # This is the default case where we either have a planar quad or an n-gon
618 int_co
= intersect_line_plane(edge
.verts
[0].co
, edge
.verts
[1].co
,
619 face
.verts
[0].co
, face
.normal
)
623 # project_point_plane
624 # Projects a point onto a plane. Returns a tuple of the projection vector
625 # and the projected coordinate
627 def project_point_plane(pt
, plane_co
, plane_no
):
629 print("project_point_plane was called")
630 proj_co
= intersect_line_plane(pt
, pt
+ plane_no
, plane_co
, plane_no
)
631 proj_ve
= proj_co
- pt
633 print("project_point_plane: proj_co is {}\nproj_ve is {}".format(proj_co
, proj_ve
))
634 return (proj_ve
, proj_co
)
637 # ------------ CHAMPHER HELPER METHODS -------------
639 def is_planar_edge(edge
, error
=0.000002):
640 angle
= edge
.calc_face_angle()
641 return ((angle
< error
and angle
> -error
) or
642 (angle
< (180 + error
) and angle
> (180 - error
)))
645 # ------------- EDGE TOOL METHODS -------------------
647 # Extends an "edge" in two directions:
648 # - Requires two vertices to be selected. They do not have to form an edge
649 # - Extends "length" in both directions
651 class Extend(Operator
):
652 bl_idname
= "mesh.edgetools_extend"
654 bl_description
= "Extend the selected edges of vertex pairs"
655 bl_options
= {'REGISTER', 'UNDO'}
659 description
="Extend the edge forwards",
664 description
="Extend the edge backwards",
667 length
= FloatProperty(
669 description
="Length to extend the edge",
674 def draw(self
, context
):
677 row
= layout
.row(align
=True)
678 row
.prop(self
, "di1", toggle
=True)
679 row
.prop(self
, "di2", toggle
=True)
681 layout
.prop(self
, "length")
684 def poll(cls
, context
):
685 ob
= context
.active_object
686 return(ob
and ob
.type == 'MESH' and context
.mode
== 'EDIT_MESH')
688 def invoke(self
, context
, event
):
689 return self
.execute(context
)
691 def execute(self
, context
):
693 me
= context
.object.data
694 bm
= bmesh
.from_edit_mesh(me
)
700 edges
= [e
for e
in bEdges
if e
.select
]
701 verts
= [v
for v
in bVerts
if v
.select
]
703 if not is_selected_enough(self
, edges
, 0, edges_n
=1, verts_n
=0, types
="Edge"):
708 vector
= e
.verts
[0].co
- e
.verts
[1].co
709 vector
.length
= self
.length
713 if (vector
[0] + vector
[1] + vector
[2]) < 0:
714 v
.co
= e
.verts
[1].co
- vector
715 newE
= bEdges
.new((e
.verts
[1], v
))
716 bEdges
.ensure_lookup_table()
718 v
.co
= e
.verts
[0].co
+ vector
719 newE
= bEdges
.new((e
.verts
[0], v
))
720 bEdges
.ensure_lookup_table()
723 if (vector
[0] + vector
[1] + vector
[2]) < 0:
724 v
.co
= e
.verts
[0].co
+ vector
725 newE
= bEdges
.new((e
.verts
[0], v
))
726 bEdges
.ensure_lookup_table()
728 v
.co
= e
.verts
[1].co
- vector
729 newE
= bEdges
.new((e
.verts
[1], v
))
730 bEdges
.ensure_lookup_table()
732 vector
= verts
[0].co
- verts
[1].co
733 vector
.length
= self
.length
737 if (vector
[0] + vector
[1] + vector
[2]) < 0:
738 v
.co
= verts
[1].co
- vector
739 e
= bEdges
.new((verts
[1], v
))
740 bEdges
.ensure_lookup_table()
742 v
.co
= verts
[0].co
+ vector
743 e
= bEdges
.new((verts
[0], v
))
744 bEdges
.ensure_lookup_table()
747 if (vector
[0] + vector
[1] + vector
[2]) < 0:
748 v
.co
= verts
[0].co
+ vector
749 e
= bEdges
.new((verts
[0], v
))
750 bEdges
.ensure_lookup_table()
752 v
.co
= verts
[1].co
- vector
753 e
= bEdges
.new((verts
[1], v
))
754 bEdges
.ensure_lookup_table()
756 bmesh
.update_edit_mesh(me
)
758 except Exception as e
:
759 error_handlers(self
, "mesh.edgetools_extend", e
,
760 reports
="Extend Operator failed", func
=False)
766 # Creates a series of edges between two edges using spline interpolation.
767 # This basically just exposes existing functionality in addition to some
768 # other common methods: Hermite (c-spline), Bezier, and b-spline. These
769 # alternates I coded myself after some extensive research into spline theory
771 # @todo Figure out what's wrong with the Blender bezier interpolation
773 class Spline(Operator
):
774 bl_idname
= "mesh.edgetools_spline"
776 bl_description
= "Create a spline interplopation between two edges"
777 bl_options
= {'REGISTER', 'UNDO'}
780 name
="Spline Algorithm",
781 items
=[('Blender', "Blender", "Interpolation provided through mathutils.geometry"),
782 ('Hermite', "C-Spline", "C-spline interpolation"),
783 ('Bezier', "Bezier", "Bezier interpolation"),
784 ('B-Spline', "B-Spline", "B-Spline interpolation")],
787 segments
= IntProperty(
789 description
="Number of segments to use in the interpolation",
794 flip1
= BoolProperty(
796 description
="Flip the direction of the spline on Edge 1",
799 flip2
= BoolProperty(
801 description
="Flip the direction of the spline on Edge 2",
804 ten1
= FloatProperty(
806 description
="Tension on Edge 1",
807 min=-4096.0, max=4096.0,
808 soft_min
=-8.0, soft_max
=8.0,
811 ten2
= FloatProperty(
813 description
="Tension on Edge 2",
814 min=-4096.0, max=4096.0,
815 soft_min
=-8.0, soft_max
=8.0,
819 def draw(self
, context
):
822 layout
.prop(self
, "alg")
823 layout
.prop(self
, "segments")
825 layout
.label("Edge 1:")
826 split
= layout
.split(percentage
=0.8, align
=True)
827 split
.prop(self
, "ten1")
828 split
.prop(self
, "flip1", text
="", icon
="ALIGN", toggle
=True)
830 layout
.label("Edge 2:")
831 split
= layout
.split(percentage
=0.8, align
=True)
832 split
.prop(self
, "ten2")
833 split
.prop(self
, "flip2", text
="", icon
="ALIGN", toggle
=True)
836 def poll(cls
, context
):
837 ob
= context
.active_object
838 return(ob
and ob
.type == 'MESH' and context
.mode
== 'EDIT_MESH')
840 def invoke(self
, context
, event
):
841 return self
.execute(context
)
843 def execute(self
, context
):
845 me
= context
.object.data
846 bm
= bmesh
.from_edit_mesh(me
)
853 edges
= [e
for e
in bEdges
if e
.select
]
855 if not is_selected_enough(self
, edges
, 0, edges_n
=2, verts_n
=0, types
="Edge"):
858 verts
= [edges
[v
// 2].verts
[v
% 2] for v
in range(4)]
863 p1_dir
= verts
[1].co
- verts
[0].co
867 p1_dir
= verts
[0].co
- verts
[1].co
870 p1_dir
.length
= -self
.ten1
872 p1_dir
.length
= self
.ten1
877 p2_dir
= verts
[2].co
- verts
[3].co
881 p2_dir
= verts
[3].co
- verts
[2].co
884 p2_dir
.length
= -self
.ten2
886 p2_dir
.length
= self
.ten2
888 # Get the interploted coordinates:
889 if self
.alg
== 'Blender':
890 pieces
= interpolate_bezier(
891 p1_co
, p1_dir
, p2_dir
, p2_co
, self
.segments
893 elif self
.alg
== 'Hermite':
894 pieces
= interpolate_line_line(
895 p1_co
, p1_dir
, p2_co
, p2_dir
, self
.segments
, 1, 'HERMITE'
897 elif self
.alg
== 'Bezier':
898 pieces
= interpolate_line_line(
899 p1_co
, p1_dir
, p2_co
, p2_dir
, self
.segments
, 1, 'BEZIER'
901 elif self
.alg
== 'B-Spline':
902 pieces
= interpolate_line_line(
903 p1_co
, p1_dir
, p2_co
, p2_dir
, self
.segments
, 1, 'BSPLINE'
908 # Add vertices and set the points:
909 for i
in range(seg
- 1):
912 bVerts
.ensure_lookup_table()
917 e
= bEdges
.new((verts
[i
], verts
[i
+ 1]))
918 bEdges
.ensure_lookup_table()
920 bmesh
.update_edit_mesh(me
)
922 except Exception as e
:
923 error_handlers(self
, "mesh.edgetools_spline", e
,
924 reports
="Spline Operator failed", func
=False)
930 # Creates edges normal to planes defined between each of two edges and the
931 # normal or the plane defined by those two edges.
932 # - Select two edges. The must form a plane.
933 # - On running the script, eight edges will be created. Delete the
934 # extras that you don't need.
935 # - The length of those edges is defined by the variable "length"
937 # @todo Change method from a cross product to a rotation matrix to make the
939 # --- todo completed 2/4/2012, but still needs work ---
940 # @todo Figure out a way to make +/- predictable
941 # - Maybe use angle between edges and vector direction definition?
942 # --- TODO COMPLETED ON 2/9/2012 ---
944 class Ortho(Operator
):
945 bl_idname
= "mesh.edgetools_ortho"
946 bl_label
= "Angle Off Edge"
947 bl_description
= "Creates new edges within an angle from vertices of selected edges"
948 bl_options
= {'REGISTER', 'UNDO'}
950 vert1
= BoolProperty(
952 description
="Enable edge creation for Vertice 1",
955 vert2
= BoolProperty(
957 description
="Enable edge creation for Vertice 2",
960 vert3
= BoolProperty(
962 description
="Enable edge creation for Vertice 3",
965 vert4
= BoolProperty(
967 description
="Enable edge creation for Vertice 4",
972 description
="Enable creation of positive direction edges",
977 description
="Enable creation of negative direction edges",
980 angle
= FloatProperty(
982 description
="Define the angle off of the originating edge",
986 length
= FloatProperty(
988 description
="Length of created edges",
992 # For when only one edge is selected (Possible feature to be testd):
993 plane
= EnumProperty(
995 items
=[("XY", "X-Y Plane", "Use the X-Y plane as the plane of creation"),
996 ("XZ", "X-Z Plane", "Use the X-Z plane as the plane of creation"),
997 ("YZ", "Y-Z Plane", "Use the Y-Z plane as the plane of creation")],
1001 def draw(self
, context
):
1002 layout
= self
.layout
1004 layout
.label("Creation:")
1005 split
= layout
.split()
1006 col
= split
.column()
1008 col
.prop(self
, "vert1", toggle
=True)
1009 col
.prop(self
, "vert2", toggle
=True)
1011 col
= split
.column()
1012 col
.prop(self
, "vert3", toggle
=True)
1013 col
.prop(self
, "vert4", toggle
=True)
1015 layout
.label("Direction:")
1016 row
= layout
.row(align
=False)
1017 row
.alignment
= 'EXPAND'
1018 row
.prop(self
, "pos")
1019 row
.prop(self
, "neg")
1023 col
= layout
.column(align
=True)
1024 col
.prop(self
, "angle")
1025 col
.prop(self
, "length")
1028 def poll(cls
, context
):
1029 ob
= context
.active_object
1030 return(ob
and ob
.type == 'MESH' and context
.mode
== 'EDIT_MESH')
1032 def invoke(self
, context
, event
):
1033 return self
.execute(context
)
1035 def execute(self
, context
):
1037 me
= context
.object.data
1038 bm
= bmesh
.from_edit_mesh(me
)
1043 edges
= [e
for e
in bEdges
if e
.select
]
1046 if not is_selected_enough(self
, edges
, 0, edges_n
=2, verts_n
=0, types
="Edge"):
1047 return {'CANCELLED'}
1049 verts
= [edges
[0].verts
[0],
1054 cos
= intersect_line_line(verts
[0].co
, verts
[1].co
, verts
[2].co
, verts
[3].co
)
1056 # If the two edges are parallel:
1058 self
.report({'WARNING'},
1059 "Selected lines are parallel: results may be unpredictable")
1060 vectors
.append(verts
[0].co
- verts
[1].co
)
1061 vectors
.append(verts
[0].co
- verts
[2].co
)
1062 vectors
.append(vectors
[0].cross(vectors
[1]))
1063 vectors
.append(vectors
[2].cross(vectors
[0]))
1064 vectors
.append(-vectors
[3])
1066 # Warn the user if they have not chosen two planar edges:
1067 if not is_same_co(cos
[0], cos
[1]):
1068 self
.report({'WARNING'},
1069 "Selected lines are not planar: results may be unpredictable")
1071 # This makes the +/- behavior predictable:
1072 if (verts
[0].co
- cos
[0]).length
< (verts
[1].co
- cos
[0]).length
:
1073 verts
[0], verts
[1] = verts
[1], verts
[0]
1074 if (verts
[2].co
- cos
[0]).length
< (verts
[3].co
- cos
[0]).length
:
1075 verts
[2], verts
[3] = verts
[3], verts
[2]
1077 vectors
.append(verts
[0].co
- verts
[1].co
)
1078 vectors
.append(verts
[2].co
- verts
[3].co
)
1080 # Normal of the plane formed by vector1 and vector2:
1081 vectors
.append(vectors
[0].cross(vectors
[1]))
1083 # Possible directions:
1084 vectors
.append(vectors
[2].cross(vectors
[0]))
1085 vectors
.append(vectors
[1].cross(vectors
[2]))
1088 vectors
[3].length
= self
.length
1089 vectors
[4].length
= self
.length
1091 # Perform any additional rotations:
1092 matrix
= Matrix
.Rotation(radians(90 + self
.angle
), 3, vectors
[2])
1093 vectors
.append(matrix
* -vectors
[3]) # vectors[5]
1094 matrix
= Matrix
.Rotation(radians(90 - self
.angle
), 3, vectors
[2])
1095 vectors
.append(matrix
* vectors
[4]) # vectors[6]
1096 vectors
.append(matrix
* vectors
[3]) # vectors[7]
1097 matrix
= Matrix
.Rotation(radians(90 + self
.angle
), 3, vectors
[2])
1098 vectors
.append(matrix
* -vectors
[4]) # vectors[8]
1100 # Perform extrusions and displacements:
1101 # There will be a total of 8 extrusions. One for each vert of each edge.
1102 # It looks like an extrusion will add the new vert to the end of the verts
1103 # list and leave the rest in the same location.
1105 # It looks like I might be able to do this within "bpy.data" with the ".add" function
1107 for v
in range(len(verts
)):
1109 if ((v
== 0 and self
.vert1
) or (v
== 1 and self
.vert2
) or
1110 (v
== 2 and self
.vert3
) or (v
== 3 and self
.vert4
)):
1114 new
.co
= vert
.co
- vectors
[5 + (v
// 2) + ((v
% 2) * 2)]
1115 bVerts
.ensure_lookup_table()
1116 bEdges
.new((vert
, new
))
1117 bEdges
.ensure_lookup_table()
1120 new
.co
= vert
.co
+ vectors
[5 + (v
// 2) + ((v
% 2) * 2)]
1121 bVerts
.ensure_lookup_table()
1122 bEdges
.new((vert
, new
))
1123 bEdges
.ensure_lookup_table()
1125 bmesh
.update_edit_mesh(me
)
1126 except Exception as e
:
1127 error_handlers(self
, "mesh.edgetools_ortho", e
,
1128 reports
="Angle Off Edge Operator failed", func
=False)
1129 return {'CANCELLED'}
1135 # Select an edge and a point or an edge and specify the radius (default is 1 BU)
1136 # You can select two edges but it might be unpredictable which edge it revolves
1137 # around so you might have to play with the switch
1139 class Shaft(Operator
):
1140 bl_idname
= "mesh.edgetools_shaft"
1142 bl_description
= "Create a shaft mesh around an axis"
1143 bl_options
= {'REGISTER', 'UNDO'}
1145 # Selection defaults:
1148 # For tracking if the user has changed selection:
1149 last_edge
= IntProperty(
1151 description
="Tracks if user has changed selected edges",
1159 description
="Edge to shaft around",
1163 flip
= BoolProperty(
1164 name
="Flip Second Edge",
1165 description
="Flip the percieved direction of the second edge",
1168 radius
= FloatProperty(
1170 description
="Shaft Radius",
1171 min=0.0, max=1024.0,
1174 start
= FloatProperty(
1175 name
="Starting Angle",
1176 description
="Angle to start the shaft at",
1177 min=-360.0, max=360.0,
1180 finish
= FloatProperty(
1181 name
="Ending Angle",
1182 description
="Angle to end the shaft at",
1183 min=-360.0, max=360.0,
1186 segments
= IntProperty(
1187 name
="Shaft Segments",
1188 description
="Number of segments to use in the shaft",
1194 def draw(self
, context
):
1195 layout
= self
.layout
1197 if self
.shaftType
== 0:
1198 layout
.prop(self
, "edge")
1199 layout
.prop(self
, "flip")
1200 elif self
.shaftType
== 3:
1201 layout
.prop(self
, "radius")
1203 layout
.prop(self
, "segments")
1204 layout
.prop(self
, "start")
1205 layout
.prop(self
, "finish")
1208 def poll(cls
, context
):
1209 ob
= context
.active_object
1210 return(ob
and ob
.type == 'MESH' and context
.mode
== 'EDIT_MESH')
1212 def invoke(self
, context
, event
):
1213 # Make sure these get reset each time we run:
1217 return self
.execute(context
)
1219 def execute(self
, context
):
1221 me
= context
.object.data
1222 bm
= bmesh
.from_edit_mesh(me
)
1230 edges
, verts
= [], []
1232 # Pre-caclulated values:
1233 rotRange
= [radians(self
.start
), radians(self
.finish
)]
1234 rads
= radians((self
.finish
- self
.start
) / self
.segments
)
1236 numV
= self
.segments
+ 1
1237 numE
= self
.segments
1239 edges
= [e
for e
in bEdges
if e
.select
]
1241 # Robustness check: there should at least be one edge selected
1242 if not is_selected_enough(self
, edges
, 0, edges_n
=1, verts_n
=0, types
="Edge"):
1243 return {'CANCELLED'}
1245 # If two edges are selected:
1251 # By default, we want to shaft around the last selected edge (it
1252 # will be the active edge). We know we are using the default if
1253 # the user has not changed which edge is being shafted around (as
1254 # is tracked by self.last_edge). When they are not the same, then
1255 # the user has changed selection.
1256 # We then need to make sure that the active object really is an edge
1257 # (robustness check)
1258 # Finally, if the active edge is not the inital one, we flip them
1259 # and have the GUI reflect that
1260 if self
.last_edge
== self
.edge
:
1261 if isinstance(bm
.select_history
.active
, bmesh
.types
.BMEdge
):
1262 if bm
.select_history
.active
!= edges
[edge
[0]]:
1263 self
.last_edge
, self
.edge
= edge
[1], edge
[1]
1264 edge
= [edge
[1], edge
[0]]
1267 self
.report({'WARNING'},
1268 "Active geometry is not an edge. Operation Cancelled")
1269 return {'CANCELLED'}
1270 elif self
.edge
== 1:
1273 verts
.append(edges
[edge
[0]].verts
[0])
1274 verts
.append(edges
[edge
[0]].verts
[1])
1279 verts
.append(edges
[edge
[1]].verts
[vert
[0]])
1280 verts
.append(edges
[edge
[1]].verts
[vert
[1]])
1283 # If there is more than one edge selected:
1284 # There are some issues with it ATM, so don't expose is it to normal users
1285 # @todo Fix edge connection ordering issue
1286 elif ENABLE_DEBUG
and len(edges
) > 2:
1287 if isinstance(bm
.select_history
.active
, bmesh
.types
.BMEdge
):
1288 active
= bm
.select_history
.active
1289 edges
.remove(active
)
1290 # Get all the verts:
1291 # edges = order_joined_edges(edges[0])
1294 if verts
.count(e
.verts
[0]) == 0:
1295 verts
.append(e
.verts
[0])
1296 if verts
.count(e
.verts
[1]) == 0:
1297 verts
.append(e
.verts
[1])
1300 self
.report({'WARNING'},
1301 "Active geometry is not an edge. Operation Cancelled")
1302 return {'CANCELLED'}
1305 verts
.append(edges
[0].verts
[0])
1306 verts
.append(edges
[0].verts
[1])
1309 if v
.select
and verts
.count(v
) == 0:
1317 # The vector denoting the axis of rotation:
1318 if self
.shaftType
== 1:
1319 axis
= active
.verts
[1].co
- active
.verts
[0].co
1321 axis
= verts
[1].co
- verts
[0].co
1323 # We will need a series of rotation matrices. We could use one which
1324 # would be faster but also might cause propagation of error
1326 # for i in range(numV):
1327 # matrices.append(Matrix.Rotation((rads * i) + rotRange[0], 3, axis))
1328 matrices
= [Matrix
.Rotation((rads
* i
) + rotRange
[0], 3, axis
) for i
in range(numV
)]
1330 # New vertice coordinates:
1333 # If two edges were selected:
1334 # - If the lines are not parallel, then it will create a cone-like shaft
1335 if self
.shaftType
== 0:
1336 for i
in range(len(verts
) - 2):
1337 init_vec
= distance_point_line(verts
[i
+ 2].co
, verts
[0].co
, verts
[1].co
)
1338 co
= init_vec
+ verts
[i
+ 2].co
1339 # These will be rotated about the orgin so will need to be shifted:
1340 for j
in range(numV
):
1341 verts_out
.append(co
- (matrices
[j
] * init_vec
))
1342 elif self
.shaftType
== 1:
1344 init_vec
= distance_point_line(i
.co
, active
.verts
[0].co
, active
.verts
[1].co
)
1345 co
= init_vec
+ i
.co
1346 # These will be rotated about the orgin so will need to be shifted:
1347 for j
in range(numV
):
1348 verts_out
.append(co
- (matrices
[j
] * init_vec
))
1349 # Else if a line and a point was selected:
1350 elif self
.shaftType
== 2:
1351 init_vec
= distance_point_line(verts
[2].co
, verts
[0].co
, verts
[1].co
)
1352 # These will be rotated about the orgin so will need to be shifted:
1354 (verts
[i
].co
- (matrices
[j
] * init_vec
)) for i
in range(2) for j
in range(numV
)
1357 # Else the above are not possible, so we will just use the edge:
1358 # - The vector defined by the edge is the normal of the plane for the shaft
1359 # - The shaft will have radius "radius"
1360 if is_axial(verts
[0].co
, verts
[1].co
) is None:
1361 proj
= (verts
[1].co
- verts
[0].co
)
1363 norm
= proj
.cross(verts
[1].co
- verts
[0].co
)
1364 vec
= norm
.cross(verts
[1].co
- verts
[0].co
)
1365 vec
.length
= self
.radius
1366 elif is_axial(verts
[0].co
, verts
[1].co
) == 'Z':
1367 vec
= verts
[0].co
+ Vector((0, 0, self
.radius
))
1369 vec
= verts
[0].co
+ Vector((0, self
.radius
, 0))
1370 init_vec
= distance_point_line(vec
, verts
[0].co
, verts
[1].co
)
1371 # These will be rotated about the orgin so will need to be shifted:
1373 (verts
[i
].co
- (matrices
[j
] * init_vec
)) for i
in range(2) for j
in range(numV
)
1376 # We should have the coordinates for a bunch of new verts
1377 # Now add the verts and build the edges and then the faces
1381 if self
.shaftType
== 1:
1383 for i
in range(numV
* len(verts
)):
1385 new
.co
= verts_out
[i
]
1386 bVerts
.ensure_lookup_table()
1388 newVerts
.append(new
)
1390 for i
in range(numE
):
1391 for j
in range(len(verts
)):
1392 e
= bEdges
.new((newVerts
[i
+ (numV
* j
)], newVerts
[i
+ (numV
* j
) + 1]))
1393 bEdges
.ensure_lookup_table()
1395 for i
in range(numV
):
1396 for j
in range(len(verts
) - 1):
1397 e
= bEdges
.new((newVerts
[i
+ (numV
* j
)], newVerts
[i
+ (numV
* (j
+ 1))]))
1398 bEdges
.ensure_lookup_table()
1401 # Faces: There is a problem with this right now
1403 for i in range(len(edges)):
1404 for j in range(numE):
1405 f = bFaces.new((newVerts[i], newVerts[i + 1],
1406 newVerts[i + (numV * j) + 1], newVerts[i + (numV * j)]))
1411 for i
in range(numV
* 2):
1413 new
.co
= verts_out
[i
]
1415 bVerts
.ensure_lookup_table()
1416 newVerts
.append(new
)
1418 for i
in range(numE
):
1419 e
= bEdges
.new((newVerts
[i
], newVerts
[i
+ 1]))
1421 bEdges
.ensure_lookup_table()
1422 e
= bEdges
.new((newVerts
[i
+ numV
], newVerts
[i
+ numV
+ 1]))
1424 bEdges
.ensure_lookup_table()
1425 for i
in range(numV
):
1426 e
= bEdges
.new((newVerts
[i
], newVerts
[i
+ numV
]))
1428 bEdges
.ensure_lookup_table()
1430 for i
in range(numE
):
1431 f
= bFaces
.new((newVerts
[i
], newVerts
[i
+ 1],
1432 newVerts
[i
+ numV
+ 1], newVerts
[i
+ numV
]))
1433 bFaces
.ensure_lookup_table()
1436 bmesh
.update_edit_mesh(me
)
1438 except Exception as e
:
1439 error_handlers(self
, "mesh.edgetools_shaft", e
,
1440 reports
="Shaft Operator failed", func
=False)
1441 return {'CANCELLED'}
1446 # "Slices" edges crossing a plane defined by a face
1448 class Slice(Operator
):
1449 bl_idname
= "mesh.edgetools_slice"
1451 bl_description
= "Cut edges at the plane defined by a selected face"
1452 bl_options
= {'REGISTER', 'UNDO'}
1454 make_copy
= BoolProperty(
1456 description
="Make new vertices at intersection points instead of spliting the edge",
1461 description
="Split into two edges that DO NOT share an intersection vertex",
1466 description
="Remove the portion on the side of the face normal",
1471 description
="Remove the portion on the side opposite of the face normal",
1475 def draw(self
, context
):
1476 layout
= self
.layout
1478 layout
.prop(self
, "make_copy")
1479 if not self
.make_copy
:
1480 layout
.prop(self
, "rip")
1481 layout
.label("Remove Side:")
1482 layout
.prop(self
, "pos")
1483 layout
.prop(self
, "neg")
1486 def poll(cls
, context
):
1487 ob
= context
.active_object
1488 return(ob
and ob
.type == 'MESH' and context
.mode
== 'EDIT_MESH')
1490 def invoke(self
, context
, event
):
1491 return self
.execute(context
)
1493 def execute(self
, context
):
1495 me
= context
.object.data
1496 bm
= bmesh
.from_edit_mesh(me
)
1503 face
, normal
= None, None
1505 # Find the selected face. This will provide the plane to project onto:
1506 # - First check to use the active face. Allows users to just
1507 # select a bunch of faces with the last being the cutting plane
1508 # - If that fails, then use the first found selected face in the BMesh face list
1509 if isinstance(bm
.select_history
.active
, bmesh
.types
.BMFace
):
1510 face
= bm
.select_history
.active
1511 normal
= bm
.select_history
.active
.normal
1512 bm
.select_history
.active
.select
= False
1521 # If we don't find a selected face exit:
1524 self
.report({'WARNING'},
1525 "Please select a face as the cutting plane. Operation Cancelled")
1526 return {'CANCELLED'}
1528 # Warn the user if they are using an n-gon might lead to some odd results
1529 elif len(face
.verts
) > 4 and not is_face_planar(face
):
1530 self
.report({'WARNING'},
1531 "Selected face is an N-gon. Results may be unpredictable")
1535 print("Number of Edges: ", len(bEdges
))
1539 print("Looping through Edges - ", dbg
)
1542 # Get the end verts on the edge:
1546 # Make sure that verts are not a part of the cutting plane:
1547 if e
.select
and (v1
not in face
.verts
and v2
not in face
.verts
):
1548 if len(face
.verts
) < 5: # Not an n-gon
1549 intersection
= intersect_line_face(e
, face
, True)
1551 intersection
= intersect_line_plane(v1
.co
, v2
.co
, face
.verts
[0].co
, normal
)
1554 print("Intersection: ", intersection
)
1556 # If an intersection exists find the distance of each of the end
1557 # points from the plane, with "positive" being in the direction
1558 # of the cutting plane's normal. If the points are on opposite
1559 # side of the plane, then it intersects and we need to cut it
1560 if intersection
is not None:
1561 bVerts
.ensure_lookup_table()
1562 bEdges
.ensure_lookup_table()
1563 bFaces
.ensure_lookup_table()
1565 d1
= distance_point_to_plane(v1
.co
, face
.verts
[0].co
, normal
)
1566 d2
= distance_point_to_plane(v2
.co
, face
.verts
[0].co
, normal
)
1567 # If they have different signs, then the edge crosses the cutting plane:
1568 if abs(d1
+ d2
) < abs(d1
- d2
):
1569 # Make the first vertex the positive one:
1575 new
.co
= intersection
1577 bVerts
.ensure_lookup_table()
1580 print("Branch rip engaged")
1581 newV1
= bVerts
.new()
1582 newV1
.co
= intersection
1583 bVerts
.ensure_lookup_table()
1585 print("newV1 created", end
='; ')
1587 newV2
= bVerts
.new()
1588 newV2
.co
= intersection
1589 bVerts
.ensure_lookup_table()
1592 print("newV2 created", end
='; ')
1594 newE1
= bEdges
.new((v1
, newV1
))
1595 newE2
= bEdges
.new((v2
, newV2
))
1596 bEdges
.ensure_lookup_table()
1599 print("new edges created", end
='; ')
1604 bEdges
.ensure_lookup_table()
1607 print("Old edge removed.\nWe're done with this edge")
1609 new
= list(bmesh
.utils
.edge_split(e
, v1
, 0.5))
1610 bEdges
.ensure_lookup_table()
1611 new
[1].co
= intersection
1613 new
[0].select
= False
1615 bEdges
.remove(new
[0])
1618 bEdges
.ensure_lookup_table()
1621 print("The Edge Loop has exited. Now to update the bmesh")
1624 bmesh
.update_edit_mesh(me
)
1626 except Exception as e
:
1627 error_handlers(self
, "mesh.edgetools_slice", e
,
1628 reports
="Slice Operator failed", func
=False)
1629 return {'CANCELLED'}
1634 # This projects the selected edges onto the selected plane
1635 # and/or both points on the selected edge
1637 class Project(Operator
):
1638 bl_idname
= "mesh.edgetools_project"
1639 bl_label
= "Project"
1640 bl_description
= ("Projects the selected Vertices/Edges onto a selected plane\n"
1641 "(Active is projected onto the rest)")
1642 bl_options
= {'REGISTER', 'UNDO'}
1644 make_copy
= BoolProperty(
1646 description
="Make duplicates of the vertices instead of altering them",
1650 def draw(self
, context
):
1651 layout
= self
.layout
1652 layout
.prop(self
, "make_copy")
1655 def poll(cls
, context
):
1656 ob
= context
.active_object
1657 return (ob
and ob
.type == 'MESH' and context
.mode
== 'EDIT_MESH')
1659 def invoke(self
, context
, event
):
1660 return self
.execute(context
)
1662 def execute(self
, context
):
1664 me
= context
.object.data
1665 bm
= bmesh
.from_edit_mesh(me
)
1673 # Find the selected face. This will provide the plane to project onto:
1674 # @todo Check first for an active face
1688 d
= distance_point_to_plane(v
.co
, fVerts
[0].co
, normal
)
1693 bVerts
.ensure_lookup_table()
1695 vector
.length
= abs(d
)
1696 v
.co
= v
.co
- (vector
* sign(d
))
1699 bmesh
.update_edit_mesh(me
)
1701 except Exception as e
:
1702 error_handlers(self
, "mesh.edgetools_project", e
,
1703 reports
="Project Operator failed", func
=False)
1705 return {'CANCELLED'}
1710 # Project_End is for projecting/extending an edge to meet a plane
1711 # This is used be selecting a face to define the plane then all the edges
1712 # Then move the vertices in the edge that is closest to the
1713 # plane to the coordinates of the intersection of the edge and the plane
1715 class Project_End(Operator
):
1716 bl_idname
= "mesh.edgetools_project_end"
1717 bl_label
= "Project (End Point)"
1718 bl_description
= ("Projects the vertices of the selected\n"
1719 "edges closest to a plane onto that plane")
1720 bl_options
= {'REGISTER', 'UNDO'}
1722 make_copy
= BoolProperty(
1724 description
="Make a duplicate of the vertice instead of moving it",
1727 keep_length
= BoolProperty(
1728 name
="Keep Edge Length",
1729 description
="Maintain edge lengths",
1732 use_force
= BoolProperty(
1733 name
="Use opposite vertices",
1734 description
="Force the usage of the vertices at the other end of the edge",
1737 use_normal
= BoolProperty(
1738 name
="Project along normal",
1739 description
="Use the plane's normal as the projection direction",
1743 def draw(self
, context
):
1744 layout
= self
.layout
1746 if not self
.keep_length
:
1747 layout
.prop(self
, "use_normal")
1748 layout
.prop(self
, "make_copy")
1749 layout
.prop(self
, "use_force")
1752 def poll(cls
, context
):
1753 ob
= context
.active_object
1754 return(ob
and ob
.type == 'MESH' and context
.mode
== 'EDIT_MESH')
1756 def invoke(self
, context
, event
):
1757 return self
.execute(context
)
1759 def execute(self
, context
):
1761 me
= context
.object.data
1762 bm
= bmesh
.from_edit_mesh(me
)
1771 # Find the selected face. This will provide the plane to project onto:
1784 if v1
in fVerts
or v2
in fVerts
:
1787 intersection
= intersect_line_plane(v1
.co
, v2
.co
, fVerts
[0].co
, normal
)
1788 if intersection
is not None:
1789 # Use abs because we don't care what side of plane we're on:
1790 d1
= distance_point_to_plane(v1
.co
, fVerts
[0].co
, normal
)
1791 d2
= distance_point_to_plane(v2
.co
, fVerts
[0].co
, normal
)
1792 # If d1 is closer than we use v1 as our vertice:
1793 # "xor" with 'use_force':
1794 if (abs(d1
) < abs(d2
)) is not self
.use_force
:
1797 v1
.co
= e
.verts
[0].co
1798 bVerts
.ensure_lookup_table()
1799 bEdges
.ensure_lookup_table()
1800 if self
.keep_length
:
1801 v1
.co
= intersection
1802 elif self
.use_normal
:
1804 vector
.length
= abs(d1
)
1805 v1
.co
= v1
.co
- (vector
* sign(d1
))
1807 v1
.co
= intersection
1811 v2
.co
= e
.verts
[1].co
1812 bVerts
.ensure_lookup_table()
1813 bEdges
.ensure_lookup_table()
1814 if self
.keep_length
:
1815 v2
.co
= intersection
1816 elif self
.use_normal
:
1818 vector
.length
= abs(d2
)
1819 v2
.co
= v2
.co
- (vector
* sign(d2
))
1821 v2
.co
= intersection
1824 bmesh
.update_edit_mesh(me
)
1826 except Exception as e
:
1827 error_handlers(self
, "mesh.edgetools_project_end", e
,
1828 reports
="Project (End Point) Operator failed", func
=False)
1829 return {'CANCELLED'}
1834 class VIEW3D_MT_edit_mesh_edgetools(Menu
):
1835 bl_label
= "Edge Tools"
1836 bl_description
= "Various tools for manipulating edges"
1838 def draw(self
, context
):
1839 layout
= self
.layout
1841 layout
.operator("mesh.edgetools_extend")
1842 layout
.operator("mesh.edgetools_spline")
1843 layout
.operator("mesh.edgetools_ortho")
1844 layout
.operator("mesh.edgetools_shaft")
1845 layout
.operator("mesh.edgetools_slice")
1848 layout
.operator("mesh.edgetools_project")
1849 layout
.operator("mesh.edgetools_project_end")
1852 # define classes for registration
1854 VIEW3D_MT_edit_mesh_edgetools
,
1865 # registering and menu integration
1868 bpy
.utils
.register_class(cls
)
1871 # unregistering and removing menus
1874 bpy
.utils
.unregister_class(cls
)
1877 if __name__
== "__main__":