Fix T52833: OBJ triangulate doesn't match viewport
[blender-addons.git] / add_advanced_objects_menu / make_struts.py
blob58e149abaefa31dcc4ade447f490889f75b34ea1
1 # Copyright (C) 2012 Bill Currie <bill@taniwha.org>
2 # Date: 2012/2/20
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 #####
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,
32 from mathutils import (
33 Vector,
34 Matrix,
35 Quaternion,
37 from math import (
38 pi, cos,
39 sin,
42 cossin = []
44 # Initialize the cossin table based on the number of segments.
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[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))
63 else:
64 up = Vector((0, 0, 1))
65 return up
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
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][0]
131 y = cossin[i][1]
132 for j in range(vps):
133 p = Vector((pos[j][0], pos[j][1] * x, pos[j][1] * 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.
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
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.
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
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[0].index].co,
194 mesh.verts[edge.verts[1].index].co,
195 None, None]
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]]
200 return v, f
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
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[0].index].co
222 c0 = v0 + od * edge.y
223 v1 = mesh.verts[edge.verts[1].index].co
224 c1 = v1 - od * edge.y
225 for i in range(n):
226 x = cossin[i][0]
227 y = cossin[i][1]
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)
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][1]
239 v[i * 2 + 1] = cyl[i][0]
240 f[i] = [None] * 4
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)
245 return v, f
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
258 # adjacant edge.
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
269 edges = bmvert.link_edges[:]
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.
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.
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[0] to
298 # verts[1] 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[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)
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[0].index == base_edge.verts[0].index or
325 self.verts[1].index == base_edge.verts[1].index):
326 axis = -self.y
327 elif (self.verts[0].index == base_edge.verts[1].index or
328 self.verts[1].index == base_edge.verts[0].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):
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()
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):
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:
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[0].index == edge2.verts[0].index:
386 axis1 = -edge1.y
387 axis2 = edge2.y
388 elif edge1.verts[1].index == edge2.verts[1].index:
389 axis1 = edge1.y
390 axis2 = -edge2.y
391 elif edge1.verts[0].index == edge2.verts[1].index:
392 axis1 = -edge1.y
393 axis2 = -edge2.y
394 elif edge1.verts[1].index == edge2.verts[0].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[0].set_frame(edge_queue[0].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.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)
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[0]]
459 v2 = truss_mesh.vertices[e.vertices[1]]
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:
481 continue
482 truss_obj.select = 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)
494 bpy.context.scene.objects.link(obj)
495 obj.select = True
496 obj.location = truss_obj.location
497 bpy.context.scene.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
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
523 manifold = BoolProperty(
524 name="Manifold",
525 description="Connect struts to form a single solid",
526 default=False
528 solid = BoolProperty(
529 name="Solid",
530 description="Create inner surface",
531 default=False
533 loops = BoolProperty(
534 name="Loops",
535 description="Create sub-surf friendly loops",
536 default=False
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
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 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()
569 try:
570 create_struts(self, context, **keywords)
571 bpy.context.user_preferences.edit.use_global_undo = store_undo
573 return {"FINISHED"}
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))
580 return {"CANCELLED"}
583 def register():
584 bpy.utils.register_module(__name__)
587 def unregister():
588 bpy.utils.unregister_module(__name__)
591 if __name__ == "__main__":
592 register()