1 #  Copyright (C) 2012 Bill Currie <bill@taniwha.org>
2 #  Date: 2012/2/20
4 # ##### BEGIN GPL LICENSE BLOCK #####
5 #
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.
10 #
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.
15 #
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.
19 #
20 # ##### END GPL LICENSE BLOCK #####
22 # <pep8 compliant>
24 import bpy
25 import bmesh
26 from bpy.types import Operator
27 from bpy.props import (
28         FloatProperty,
29         IntProperty,
30         BoolProperty,
31         )
32 from mathutils import (
33         Vector,
34         Matrix,
35         Quaternion,
36         )
37 from math import (
38         pi, cos,
39         sin,
40         )
42 cossin = []
44 # Initialize the cossin table based on the number of segments.
45 #
46 #   @param n  The number of segments into which the circle will be
47 #             divided.
48 #   @return   None
51 def build_cossin(n):
52     global cossin
53     cossin = []
54     for i in range(n):
55         a = 2 * pi * i / n
56         cossin.append((cos(a), sin(a)))
59 def select_up(axis):
60     # if axis.length != 0 and (abs(axis / axis.length) < 1e-5 and abs(axis / axis.length) < 1e-5):
61     if (abs(axis / axis.length) < 1e-5 and abs(axis / axis.length) < 1e-5):
62         up = Vector((-1, 0, 0))
63     else:
64         up = Vector((0, 0, 1))
65     return up
67 # Make a single strut in non-manifold mode.
68 #
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.
77 #
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
80 #                   center-line.
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):
100     v1 = Vector(v1)
101     v2 = Vector(v2)
102     axis = v2 - v1
103     pos = [(0, od / 2)]
104     if loops:
105         pos += [((od - ind) / 2, od / 2),
106                 (axis.length - (od - ind) / 2, od / 2)]
107     pos += [(axis.length, od / 2)]
108     if solid:
109         pos += [(axis.length, ind / 2)]
110         if loops:
111             pos += [(axis.length - (od - ind) / 2, ind / 2),
112                     ((od - ind) / 2, ind / 2)]
113         pos += [(0, ind / 2)]
114     vps = len(pos)
115     fps = vps
116     if not solid:
117         fps -= 1
118     fw = axis.copy()
119     fw.normalize()
120     up = select_up(axis)
121     lf = up.cross(fw)
122     lf.normalize()
123     up = fw.cross(lf)
124     mat = Matrix((fw, lf, up))
125     mat.transpose()
126     verts = [None] * n * vps
127     faces = [None] * n * fps
128     for i in range(n):
129         base = (i - 1) * vps
130         x = cossin[i]
131         y = cossin[i]
132         for j in range(vps):
133             p = Vector((pos[j], pos[j] * x, pos[j] * y))
134             p = mat * p
135             verts[i * vps + j] = p + v1
136         if i:
137             for j in range(fps):
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
142     i = n
143     for j in range(fps):
144         f = (i - 1) * fps + j
145         faces[f] = [base + j, j, (j + 1) % vps, base + (j + 1) % vps]
147     return verts, faces
150 # Project a point along a vector onto a plane.
151 #
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.
156 #
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
160 #                   projected.
161 #   @param p        A point through which the plane passes.
162 #   @return         A vector representing the projected point, or the
163 #                   original point.
165 def project_point(point, dir, norm, p):
166     d = (point - p).dot(norm)
167     if d >= 0:
168         # the point is already on or in front of the plane
169         return point
170     v = dir.dot(norm)
171     if v * v < 1e-8:
172         # the plane is unreachable
173         return point
174     return point - dir * d / v
177 # Make a simple strut for debugging.
178 #
179 #   The strut is just a single quad representing the Z axis of the edge.
180 #
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
183 #                   indices.
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
188 #                   number.
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.index].co,
194          mesh.verts[edge.verts.index].co,
195          None, None]
196     v = v + edge.z * od / 2
197     v = v + edge.z * od / 2
198     f = [[edge_num * 4 + 0, edge_num * 4 + 1,
199           edge_num * 4 + 2, edge_num * 4 + 3]]
200     return v, f
203 # Make a cylinder with ends clipped to the end-planes of the edge.
204 #
205 #   The strut is just a single quad representing the Z axis of the edge.
206 #
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
209 #                   indices.
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
214 #                   number.
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):
219     n = len(cossin)
220     cyl = [None] * n
221     v0 = mesh.verts[edge.verts.index].co
222     c0 = v0 + od * edge.y
223     v1 = mesh.verts[edge.verts.index].co
224     c1 = v1 - od * edge.y
225     for i in range(n):
226         x = cossin[i]
227         y = cossin[i]
228         r = (edge.z * x - edge.x * y) * od / 2
229         cyl[i] = [c0 + r, c1 + r]
230         for p in edge.verts.planes:
231             cyl[i] = project_point(cyl[i], edge.y, p, v0)
232         for p in edge.verts.planes:
233             cyl[i] = project_point(cyl[i], -edge.y, p, v1)
234     v = [None] * n * 2
235     f = [None] * n
236     base = edge_num * n * 2
237     for i in range(n):
238         v[i * 2 + 0] = cyl[i]
239         v[i * 2 + 1] = cyl[i]
240         f[i] = [None] * 4
241         f[i] = base + i * 2 + 0
242         f[i] = base + i * 2 + 1
243         f[i] = base + (i * 2 + 3) % (n * 2)
244         f[i] = base + (i * 2 + 2) % (n * 2)
245     return v, f
248 # Represent a vertex in the base mesh, with additional information.
249 #
250 #   These vertices are @b not shared between edges.
251 #
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
260 class SVert:
261     # Create a vertex holding additional information about the bmesh vertex.
262     #   @param bmvert   The bmesh vertex for which additional information is
263     #                   to be stored.
264     #   @param bmedge   The edge to which this vertex is attached.
266     def __init__(self, bmvert, bmedge, edge):
267         self.index = bmvert.index
268         self.edge = edge
270         edges.remove(bmedge)
271         self.edges = tuple(map(lambda e: e.index, edges))
272         self.planes = []
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.
280 #
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
285 #   edge.
286 #
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.
295 #                   Initially invalid.
296 #   @var y          The y axis of the edges local frame of reference.
297 #                   Initialized such that the edge runs from verts to
298 #                   verts along the negative y axis.
299 #   @var z          The z axis of the edges local frame of reference.
300 #                   Initially invalid.
303 class SEdge:
305     def __init__(self, bmesh, bmedge):
307         self.index = bmedge.index
308         self.bmedge = bmedge
309         bmesh.verts.ensure_lookup_table()
310         self.verts = (SVert(bmedge.verts, bmedge, self),
311                       SVert(bmedge.verts, bmedge, self))
312         self.y = (bmesh.verts[self.verts.index].co -
313                   bmesh.verts[self.verts.index].co)
314         self.y.normalize()
315         self.x = self.z = None
317     def set_frame(self, up):
318         self.x = self.y.cross(up)
319         self.x.normalize()
320         self.z = self.x.cross(self.y)
322     def calc_frame(self, base_edge):
323         baxis = base_edge.y
324         if (self.verts.index == base_edge.verts.index or
325               self.verts.index == base_edge.verts.index):
326             axis = -self.y
327         elif (self.verts.index == base_edge.verts.index or
328                 self.verts.index == base_edge.verts.index):
329             axis = self.y
330         else:
331             raise ValueError("edges not connected")
332         if baxis.dot(axis) in (-1, 1):
333             # aligned axis have their up/z aligned
334             up = base_edge.z
335         else:
336             # Get the unit vector dividing the angle (theta) between baxis and
337             # axis in two equal parts
338             h = (baxis + axis)
339             h.normalize()
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:)
345             up = q * base_edge.z
346         self.set_frame(up)
348     def calc_vert_planes(self, edges):
349         for v in self.verts:
350             v.calc_planes(edges)
352     def bisect_faces(self):
356             return (n1 + n2).normalized()
357         return n1
359     def calc_simple_frame(self):
360         return self.y.cross(select_up(self.y)).normalized()
362     def find_edge_frame(self, sedges):
364             return self.bisect_faces()
365         if self.verts.edges or self.verts.edges:
366             edges = list(self.verts.edges + self.verts.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:
370                 edges.pop()
371             if not edges:
372                 return self.calc_simple_frame()
373             n1 = edges[-1].y.cross(self.y).normalized()
374             edges.pop()
375             while edges and edges[-1].y.cross(self.y).cross(n1).length < 1e-3:
376                 edges.pop()
377             if not edges:
378                 return n1
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.index == edge2.verts.index:
386         axis1 = -edge1.y
387         axis2 = edge2.y
388     elif edge1.verts.index == edge2.verts.index:
389         axis1 = edge1.y
390         axis2 = -edge2.y
391     elif edge1.verts.index == edge2.verts.index:
392         axis1 = -edge1.y
393         axis2 = -edge2.y
394     elif edge1.verts.index == edge2.verts.index:
395         axis1 = edge1.y
396         axis2 = edge2.y
397     else:
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)
407     while edge_set:
408         edge_queue = [edge_set.pop()]
409         edge_queue.set_frame(edge_queue.find_edge_frame(edges))
410         while edge_queue:
411             current_edge = edge_queue.pop()
412             for i in (0, 1):
413                 for e in current_edge.verts[i].edges:
414                     edge = edges[e]
415                     if edge.x is not None:  # edge already processed
416                         continue
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.view_layer.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)
431     verts = []
432     faces = []
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)
437         verts += v
438         faces += f
439     return verts, faces
442 def make_simple_struts(truss_mesh, ind, od, segments, solid, loops):
443     vps = 2
444     if solid:
445         vps *= 2
446     if loops:
447         vps *= 2
448     fps = vps
449     if not solid:
450         fps -= 1
452     verts = [None] * len(truss_mesh.edges) * segments * vps
453     faces = [None] * len(truss_mesh.edges) * segments * fps
454     vbase = 0
455     fbase = 0
457     for e in truss_mesh.edges:
458         v1 = truss_mesh.vertices[e.vertices]
459         v2 = truss_mesh.vertices[e.vertices]
460         v, f = make_strut(v1.co, v2.co, ind, od, segments, solid, loops)
461         for fv in f:
462             for i in range(len(fv)):
463                 fv[i] += vbase
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
473     return verts, faces
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_get():
481             continue
482         truss_obj.select_set(False)
483         truss_mesh = truss_obj.to_mesh(context.scene, True, 'PREVIEW')
484         if not truss_mesh.edges:
485             continue
486         if manifold:
487             verts, faces = make_manifold_struts(truss_obj, od, segments)
488         else:
489             verts, faces = make_simple_struts(truss_mesh, ind, od, segments,
490                                               solid, loops)
491         mesh = bpy.data.meshes.new("Struts")
492         mesh.from_pydata(verts, [], faces)
493         obj = bpy.data.objects.new("Struts", mesh)
495         obj.select_set(True)
496         obj.location = truss_obj.location
497         bpy.context.view_layer.objects.active = obj
498         mesh.update()
501 class Struts(Operator):
502     bl_idname = "mesh.generate_struts"
503     bl_label = "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'}
509     ind: FloatProperty(
510             name="Inside Diameter",
511             description="Diameter of inner surface",
512             min=0.0, soft_min=0.0,
513             max=100, soft_max=100,
514             default=0.04
515             )
516     od: FloatProperty(
517             name="Outside Diameter",
518             description="Diameter of outer surface",
519             min=0.001, soft_min=0.001,
520             max=100, soft_max=100,
521             default=0.05
522             )
523     manifold: BoolProperty(
524             name="Manifold",
525             description="Connect struts to form a single solid",
526             default=False
527             )
528     solid: BoolProperty(
529             name="Solid",
530             description="Create inner surface",
531             default=False
532             )
533     loops: BoolProperty(
534             name="Loops",
535             description="Create sub-surf friendly loops",
536             default=False
537             )
538     segments: IntProperty(
539             name="Segments",
540             description="Number of segments around strut",
541             min=3, soft_min=3,
542             max=64, soft_max=64,
543             default=12
544             )
546     def draw(self, context):
547         layout = self.layout
549         col = layout.column(align=True)
550         col.prop(self, "ind")
551         col.prop(self, "od")
552         col.prop(self, "segments")
553         col.separator()
555         col.prop(self, "manifold")
556         col.prop(self, "solid")
557         col.prop(self, "loops")
559     @classmethod
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         keywords = self.as_keywords()
567         try:
568             create_struts(self, context, **keywords)
570             return {"FINISHED"}
572         except Exception as e:
573             self.report({"WARNING"},
574                         "Make Struts could not be performed. Operation Cancelled")
575             print("\n[mesh.generate_struts]\n{}".format(e))
576             return {"CANCELLED"}
579 def register():
580     bpy.utils.register_module(__name__)
583 def unregister():
584     bpy.utils.unregister_module(__name__)
587 if __name__ == "__main__":
588     register()