1 # Copyright (C) 2012 Bill Currie <bill@taniwha.org>
4 # ##### BEGIN GPL LICENSE BLOCK #####
6 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; either version 2
9 # of the License, or (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, write to the Free Software Foundation,
18 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20 # ##### END GPL LICENSE BLOCK #####
26 from bpy
.types
import Operator
27 from bpy
.props
import (
32 from mathutils
import (
44 # Initialize the cossin table based on the number of segments.
46 # @param n The number of segments into which the circle will be
56 cossin
.append((cos(a
), sin(a
)))
60 # if axis.length != 0 and (abs(axis[0] / axis.length) < 1e-5 and abs(axis[1] / axis.length) < 1e-5):
61 if (abs(axis
[0] / axis
.length
) < 1e-5 and abs(axis
[1] / axis
.length
) < 1e-5):
62 up
= Vector((-1, 0, 0))
64 up
= Vector((0, 0, 1))
67 # Make a single strut in non-manifold mode.
69 # The strut will be a "cylinder" with @a n sides. The vertices of the
70 # cylinder will be @a od / 2 from the center of the cylinder. Optionally,
71 # extra loops will be placed (@a od - @a id) / 2 from either end. The
72 # strut will be either a simple, open-ended single-surface "cylinder", or a
73 # double walled "pipe" with the outer wall vertices @a od / 2 from the center
74 # and the inner wall vertices @a id / 2 from the center. The two walls will
75 # be joined together at the ends with a face ring such that the entire strut
76 # is a manifold object. All faces of the strut will be quads.
78 # @param v1 Vertex representing one end of the strut's center-line.
79 # @param v2 Vertex representing the other end of the strut's
81 # @param id The diameter of the inner wall of a solid strut. Used for
82 # calculating the position of the extra loops irrespective
83 # of the solidity of the strut.
84 # @param od The diameter of the outer wall of a solid strut, or the
85 # diameter of a non-solid strut.
86 # @param solid If true, the strut will be made solid such that it has an
87 # inner wall (diameter @a id), an outer wall (diameter
88 # @a od), and face rings at either end of the strut such
89 # the strut is a manifold object. If false, the strut is
90 # a simple, open-ended "cylinder".
91 # @param loops If true, edge loops will be placed at either end of the
92 # strut, (@a od - @a id) / 2 from the end of the strut. The
93 # loops make subsurfed solid struts work nicely.
94 # @return A tuple containing a list of vertices and a list of faces.
95 # The face vertex indices are accurate only for the list of
96 # vertices for the created strut.
99 def make_strut(v1
, v2
, ind
, od
, n
, solid
, loops
):
105 pos
+= [((od
- ind
) / 2, od
/ 2),
106 (axis
.length
- (od
- ind
) / 2, od
/ 2)]
107 pos
+= [(axis
.length
, od
/ 2)]
109 pos
+= [(axis
.length
, ind
/ 2)]
111 pos
+= [(axis
.length
- (od
- ind
) / 2, ind
/ 2),
112 ((od
- ind
) / 2, ind
/ 2)]
113 pos
+= [(0, ind
/ 2)]
124 mat
= Matrix((fw
, lf
, up
))
126 verts
= [None] * n
* vps
127 faces
= [None] * n
* fps
133 p
= Vector((pos
[j
][0], pos
[j
][1] * x
, pos
[j
][1] * y
))
135 verts
[i
* vps
+ j
] = p
+ v1
138 f
= (i
- 1) * fps
+ j
139 faces
[f
] = [base
+ j
, base
+ vps
+ j
,
140 base
+ vps
+ (j
+ 1) % vps
, base
+ (j
+ 1) % vps
]
141 base
= len(verts
) - vps
144 f
= (i
- 1) * fps
+ j
145 faces
[f
] = [base
+ j
, j
, (j
+ 1) % vps
, base
+ (j
+ 1) % vps
]
150 # Project a point along a vector onto a plane.
152 # Really, just find the intersection of the line represented by @a point
153 # and @a dir with the plane represented by @a norm and @a p. However, if
154 # the point is on or in front of the plane, or the line is parallel to
155 # the plane, the original point will be returned.
157 # @param point The point to be projected onto the plane.
158 # @param dir The vector along which the point will be projected.
159 # @param norm The normal of the plane onto which the point will be
161 # @param p A point through which the plane passes.
162 # @return A vector representing the projected point, or the
165 def project_point(point
, dir, norm
, p
):
166 d
= (point
- p
).dot(norm
)
168 # the point is already on or in front of the plane
172 # the plane is unreachable
174 return point
- dir * d
/ v
177 # Make a simple strut for debugging.
179 # The strut is just a single quad representing the Z axis of the edge.
181 # @param mesh The base mesh. Used for finding the edge vertices.
182 # @param edge_num The number of the current edge. For the face vertex
184 # @param edge The edge for which the strut will be built.
185 # @param od Twice the width of the strut.
186 # @return A tuple containing a list of vertices and a list of faces.
187 # The face vertex indices are pre-adjusted by the edge
189 # @fixme The face vertex indices should be accurate for the local
190 # vertices (consistency)
192 def make_debug_strut(mesh
, edge_num
, edge
, od
):
193 v
= [mesh
.verts
[edge
.verts
[0].index
].co
,
194 mesh
.verts
[edge
.verts
[1].index
].co
,
196 v
[2] = v
[1] + edge
.z
* od
/ 2
197 v
[3] = v
[0] + edge
.z
* od
/ 2
198 f
= [[edge_num
* 4 + 0, edge_num
* 4 + 1,
199 edge_num
* 4 + 2, edge_num
* 4 + 3]]
203 # Make a cylinder with ends clipped to the end-planes of the edge.
205 # The strut is just a single quad representing the Z axis of the edge.
207 # @param mesh The base mesh. Used for finding the edge vertices.
208 # @param edge_num The number of the current edge. For the face vertex
210 # @param edge The edge for which the strut will be built.
211 # @param od The diameter of the strut.
212 # @return A tuple containing a list of vertices and a list of faces.
213 # The face vertex indices are pre-adjusted by the edge
215 # @fixme The face vertex indices should be accurate for the local
216 # vertices (consistency)
218 def make_clipped_cylinder(mesh
, edge_num
, edge
, od
):
221 v0
= mesh
.verts
[edge
.verts
[0].index
].co
222 c0
= v0
+ od
* edge
.y
223 v1
= mesh
.verts
[edge
.verts
[1].index
].co
224 c1
= v1
- od
* edge
.y
228 r
= (edge
.z
* x
- edge
.x
* y
) * od
/ 2
229 cyl
[i
] = [c0
+ r
, c1
+ r
]
230 for p
in edge
.verts
[0].planes
:
231 cyl
[i
][0] = project_point(cyl
[i
][0], edge
.y
, p
, v0
)
232 for p
in edge
.verts
[1].planes
:
233 cyl
[i
][1] = project_point(cyl
[i
][1], -edge
.y
, p
, v1
)
236 base
= edge_num
* n
* 2
238 v
[i
* 2 + 0] = cyl
[i
][1]
239 v
[i
* 2 + 1] = cyl
[i
][0]
241 f
[i
][0] = base
+ i
* 2 + 0
242 f
[i
][1] = base
+ i
* 2 + 1
243 f
[i
][2] = base
+ (i
* 2 + 3) % (n
* 2)
244 f
[i
][3] = base
+ (i
* 2 + 2) % (n
* 2)
248 # Represent a vertex in the base mesh, with additional information.
250 # These vertices are @b not shared between edges.
252 # @var index The index of the vert in the base mesh
253 # @var edge The edge to which this vertex is attached.
254 # @var edges A tuple of indicess of edges attached to this vert, not
255 # including the edge to which this vertex is attached.
256 # @var planes List of vectors representing the normals of the planes that
257 # bisect the angle between this vert's edge and each other
261 # Create a vertex holding additional information about the bmesh vertex.
262 # @param bmvert The bmesh vertex for which additional information is
264 # @param bmedge The edge to which this vertex is attached.
266 def __init__(self
, bmvert
, bmedge
, edge
):
267 self
.index
= bmvert
.index
269 edges
= bmvert
.link_edges
[:]
271 self
.edges
= tuple(map(lambda e
: e
.index
, edges
))
274 def calc_planes(self
, edges
):
275 for ed
in self
.edges
:
276 self
.planes
.append(calc_plane_normal(self
.edge
, edges
[ed
]))
279 # Represent an edge in the base mesh, with additional information.
281 # Edges do not share vertices so that the edge is always on the front (back?
282 # must verify) side of all the planes attached to its vertices. If the
283 # vertices were shared, the edge could be on either side of the planes, and
284 # there would be planes attached to the vertex that are irrelevant to the
287 # @var index The index of the edge in the base mesh.
288 # @var bmedge Cached reference to this edge's bmedge
289 # @var verts A tuple of 2 SVert vertices, one for each end of the
290 # edge. The vertices are @b not shared between edges.
291 # However, if two edges are connected via a vertex in the
292 # bmesh, their corresponding SVert vertices will have the
293 # the same index value.
294 # @var x The x axis of the edges local frame of reference.
296 # @var y The y axis of the edges local frame of reference.
297 # Initialized such that the edge runs from verts[0] to
298 # verts[1] along the negative y axis.
299 # @var z The z axis of the edges local frame of reference.
305 def __init__(self
, bmesh
, bmedge
):
307 self
.index
= bmedge
.index
309 bmesh
.verts
.ensure_lookup_table()
310 self
.verts
= (SVert(bmedge
.verts
[0], bmedge
, self
),
311 SVert(bmedge
.verts
[1], bmedge
, self
))
312 self
.y
= (bmesh
.verts
[self
.verts
[0].index
].co
-
313 bmesh
.verts
[self
.verts
[1].index
].co
)
315 self
.x
= self
.z
= None
317 def set_frame(self
, up
):
318 self
.x
= self
.y
.cross(up
)
320 self
.z
= self
.x
.cross(self
.y
)
322 def calc_frame(self
, base_edge
):
324 if (self
.verts
[0].index
== base_edge
.verts
[0].index
or
325 self
.verts
[1].index
== base_edge
.verts
[1].index
):
327 elif (self
.verts
[0].index
== base_edge
.verts
[1].index
or
328 self
.verts
[1].index
== base_edge
.verts
[0].index
):
331 raise ValueError("edges not connected")
332 if baxis
.dot(axis
) in (-1, 1):
333 # aligned axis have their up/z aligned
336 # Get the unit vector dividing the angle (theta) between baxis and
337 # axis in two equal parts
340 # (cos(theta/2), sin(theta/2) * n) where n is the unit vector of the
341 # axis rotating baxis onto axis
342 q
= Quaternion([baxis
.dot(h
)] + list(baxis
.cross(h
)))
343 # rotate the base edge's up around the rotation axis (blender
344 # quaternion shortcut:)
348 def calc_vert_planes(self
, edges
):
352 def bisect_faces(self
):
353 n1
= self
.bmedge
.link_faces
[0].normal
354 if len(self
.bmedge
.link_faces
) > 1:
355 n2
= self
.bmedge
.link_faces
[1].normal
356 return (n1
+ n2
).normalized()
359 def calc_simple_frame(self
):
360 return self
.y
.cross(select_up(self
.y
)).normalized()
362 def find_edge_frame(self
, sedges
):
363 if self
.bmedge
.link_faces
:
364 return self
.bisect_faces()
365 if self
.verts
[0].edges
or self
.verts
[1].edges
:
366 edges
= list(self
.verts
[0].edges
+ self
.verts
[1].edges
)
367 for i
in range(len(edges
)):
368 edges
[i
] = sedges
[edges
[i
]]
369 while edges
and edges
[-1].y
.cross(self
.y
).length
< 1e-3:
372 return self
.calc_simple_frame()
373 n1
= edges
[-1].y
.cross(self
.y
).normalized()
375 while edges
and edges
[-1].y
.cross(self
.y
).cross(n1
).length
< 1e-3:
379 n2
= edges
[-1].y
.cross(self
.y
).normalized()
380 return (n1
+ n2
).normalized()
381 return self
.calc_simple_frame()
384 def calc_plane_normal(edge1
, edge2
):
385 if edge1
.verts
[0].index
== edge2
.verts
[0].index
:
388 elif edge1
.verts
[1].index
== edge2
.verts
[1].index
:
391 elif edge1
.verts
[0].index
== edge2
.verts
[1].index
:
394 elif edge1
.verts
[1].index
== edge2
.verts
[0].index
:
398 raise ValueError("edges not connected")
399 # Both axis1 and axis2 are unit vectors, so this will produce a vector
400 # bisects the two, so long as they are not 180 degrees apart (in which
401 # there are infinite solutions).
402 return (axis1
+ axis2
).normalized()
405 def build_edge_frames(edges
):
406 edge_set
= set(edges
)
408 edge_queue
= [edge_set
.pop()]
409 edge_queue
[0].set_frame(edge_queue
[0].find_edge_frame(edges
))
411 current_edge
= edge_queue
.pop()
413 for e
in current_edge
.verts
[i
].edges
:
415 if edge
.x
is not None: # edge already processed
417 edge_set
.remove(edge
)
418 edge_queue
.append(edge
)
419 edge
.calc_frame(current_edge
)
422 def make_manifold_struts(truss_obj
, od
, segments
):
423 bpy
.context
.scene
.objects
.active
= truss_obj
424 bpy
.ops
.object.editmode_toggle()
425 truss_mesh
= bmesh
.from_edit_mesh(truss_obj
.data
).copy()
426 bpy
.ops
.object.editmode_toggle()
427 edges
= [None] * len(truss_mesh
.edges
)
428 for i
, e
in enumerate(truss_mesh
.edges
):
429 edges
[i
] = SEdge(truss_mesh
, e
)
430 build_edge_frames(edges
)
433 for e
, edge
in enumerate(edges
):
434 # v, f = make_debug_strut(truss_mesh, e, edge, od)
435 edge
.calc_vert_planes(edges
)
436 v
, f
= make_clipped_cylinder(truss_mesh
, e
, edge
, od
)
442 def make_simple_struts(truss_mesh
, ind
, od
, segments
, solid
, loops
):
452 verts
= [None] * len(truss_mesh
.edges
) * segments
* vps
453 faces
= [None] * len(truss_mesh
.edges
) * segments
* fps
457 for e
in truss_mesh
.edges
:
458 v1
= truss_mesh
.vertices
[e
.vertices
[0]]
459 v2
= truss_mesh
.vertices
[e
.vertices
[1]]
460 v
, f
= make_strut(v1
.co
, v2
.co
, ind
, od
, segments
, solid
, loops
)
462 for i
in range(len(fv
)):
464 for i
in range(len(v
)):
465 verts
[vbase
+ i
] = v
[i
]
466 for i
in range(len(f
)):
467 faces
[fbase
+ i
] = f
[i
]
468 # if not base % 12800:
469 # print (base * 100 / len(verts))
470 vbase
+= vps
* segments
471 fbase
+= fps
* segments
476 def create_struts(self
, context
, ind
, od
, segments
, solid
, loops
, manifold
):
477 build_cossin(segments
)
479 for truss_obj
in bpy
.context
.scene
.objects
:
480 if not truss_obj
.select
:
482 truss_obj
.select
= False
483 truss_mesh
= truss_obj
.to_mesh(context
.scene
, True, 'PREVIEW')
484 if not truss_mesh
.edges
:
487 verts
, faces
= make_manifold_struts(truss_obj
, od
, segments
)
489 verts
, faces
= make_simple_struts(truss_mesh
, ind
, od
, segments
,
491 mesh
= bpy
.data
.meshes
.new("Struts")
492 mesh
.from_pydata(verts
, [], faces
)
493 obj
= bpy
.data
.objects
.new("Struts", mesh
)
494 bpy
.context
.scene
.objects
.link(obj
)
496 obj
.location
= truss_obj
.location
497 bpy
.context
.scene
.objects
.active
= obj
501 class Struts(Operator
):
502 bl_idname
= "mesh.generate_struts"
504 bl_description
= ("Add one or more struts meshes based on selected truss meshes \n"
505 "Note: can get very high poly\n"
506 "Needs an existing Active Mesh Object")
507 bl_options
= {'REGISTER', 'UNDO'}
510 name
="Inside Diameter",
511 description
="Diameter of inner surface",
512 min=0.0, soft_min
=0.0,
513 max=100, soft_max
=100,
517 name
="Outside Diameter",
518 description
="Diameter of outer surface",
519 min=0.001, soft_min
=0.001,
520 max=100, soft_max
=100,
523 manifold
= BoolProperty(
525 description
="Connect struts to form a single solid",
528 solid
= BoolProperty(
530 description
="Create inner surface",
533 loops
= BoolProperty(
535 description
="Create sub-surf friendly loops",
538 segments
= IntProperty(
540 description
="Number of segments around strut",
546 def draw(self
, context
):
549 col
= layout
.column(align
=True)
550 col
.prop(self
, "ind")
552 col
.prop(self
, "segments")
555 col
.prop(self
, "manifold")
556 col
.prop(self
, "solid")
557 col
.prop(self
, "loops")
560 def poll(cls
, context
):
561 obj
= context
.active_object
562 return obj
is not None and obj
.type == "MESH"
564 def execute(self
, context
):
565 store_undo
= bpy
.context
.user_preferences
.edit
.use_global_undo
566 bpy
.context
.user_preferences
.edit
.use_global_undo
= False
567 keywords
= self
.as_keywords()
570 create_struts(self
, context
, **keywords
)
571 bpy
.context
.user_preferences
.edit
.use_global_undo
= store_undo
575 except Exception as e
:
576 bpy
.context
.user_preferences
.edit
.use_global_undo
= store_undo
577 self
.report({"WARNING"},
578 "Make Struts could not be performed. Operation Cancelled")
579 print("\n[mesh.generate_struts]\n{}".format(e
))
584 bpy
.utils
.register_module(__name__
)
588 bpy
.utils
.unregister_module(__name__
)
591 if __name__
== "__main__":