diff options
author | meta-androcto <meta.androcto1@gmail.com> | 2017-06-15 15:06:00 +0300 |
---|---|---|
committer | meta-androcto <meta.androcto1@gmail.com> | 2017-06-15 15:06:00 +0300 |
commit | c6676127556e5756e4c94f39784d27149f2eb86d (patch) | |
tree | a97b100b59224a3c3b8954b891968c4afa9bea79 /add_advanced_objects_menu/make_struts.py | |
parent | 17d293687324e86b2e94e6ca3574e294f3da3667 (diff) |
add advanced objects: split to 2 folders menu and panel
Diffstat (limited to 'add_advanced_objects_menu/make_struts.py')
-rw-r--r-- | add_advanced_objects_menu/make_struts.py | 592 |
1 files changed, 592 insertions, 0 deletions
diff --git a/add_advanced_objects_menu/make_struts.py b/add_advanced_objects_menu/make_struts.py new file mode 100644 index 00000000..58e149ab --- /dev/null +++ b/add_advanced_objects_menu/make_struts.py @@ -0,0 +1,592 @@ +# Copyright (C) 2012 Bill Currie <bill@taniwha.org> +# Date: 2012/2/20 + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +# <pep8 compliant> + +import bpy +import bmesh +from bpy.types import Operator +from bpy.props import ( + FloatProperty, + IntProperty, + BoolProperty, + ) +from mathutils import ( + Vector, + Matrix, + Quaternion, + ) +from math import ( + pi, cos, + sin, + ) + +cossin = [] + +# Initialize the cossin table based on the number of segments. +# +# @param n The number of segments into which the circle will be +# divided. +# @return None + + +def build_cossin(n): + global cossin + cossin = [] + for i in range(n): + a = 2 * pi * i / n + cossin.append((cos(a), sin(a))) + + +def select_up(axis): + # if axis.length != 0 and (abs(axis[0] / axis.length) < 1e-5 and abs(axis[1] / axis.length) < 1e-5): + if (abs(axis[0] / axis.length) < 1e-5 and abs(axis[1] / axis.length) < 1e-5): + up = Vector((-1, 0, 0)) + else: + up = Vector((0, 0, 1)) + return up + +# Make a single strut in non-manifold mode. +# +# The strut will be a "cylinder" with @a n sides. The vertices of the +# cylinder will be @a od / 2 from the center of the cylinder. Optionally, +# extra loops will be placed (@a od - @a id) / 2 from either end. The +# strut will be either a simple, open-ended single-surface "cylinder", or a +# double walled "pipe" with the outer wall vertices @a od / 2 from the center +# and the inner wall vertices @a id / 2 from the center. The two walls will +# be joined together at the ends with a face ring such that the entire strut +# is a manifold object. All faces of the strut will be quads. +# +# @param v1 Vertex representing one end of the strut's center-line. +# @param v2 Vertex representing the other end of the strut's +# center-line. +# @param id The diameter of the inner wall of a solid strut. Used for +# calculating the position of the extra loops irrespective +# of the solidity of the strut. +# @param od The diameter of the outer wall of a solid strut, or the +# diameter of a non-solid strut. +# @param solid If true, the strut will be made solid such that it has an +# inner wall (diameter @a id), an outer wall (diameter +# @a od), and face rings at either end of the strut such +# the strut is a manifold object. If false, the strut is +# a simple, open-ended "cylinder". +# @param loops If true, edge loops will be placed at either end of the +# strut, (@a od - @a id) / 2 from the end of the strut. The +# loops make subsurfed solid struts work nicely. +# @return A tuple containing a list of vertices and a list of faces. +# The face vertex indices are accurate only for the list of +# vertices for the created strut. + + +def make_strut(v1, v2, ind, od, n, solid, loops): + v1 = Vector(v1) + v2 = Vector(v2) + axis = v2 - v1 + pos = [(0, od / 2)] + if loops: + pos += [((od - ind) / 2, od / 2), + (axis.length - (od - ind) / 2, od / 2)] + pos += [(axis.length, od / 2)] + if solid: + pos += [(axis.length, ind / 2)] + if loops: + pos += [(axis.length - (od - ind) / 2, ind / 2), + ((od - ind) / 2, ind / 2)] + pos += [(0, ind / 2)] + vps = len(pos) + fps = vps + if not solid: + fps -= 1 + fw = axis.copy() + fw.normalize() + up = select_up(axis) + lf = up.cross(fw) + lf.normalize() + up = fw.cross(lf) + mat = Matrix((fw, lf, up)) + mat.transpose() + verts = [None] * n * vps + faces = [None] * n * fps + for i in range(n): + base = (i - 1) * vps + x = cossin[i][0] + y = cossin[i][1] + for j in range(vps): + p = Vector((pos[j][0], pos[j][1] * x, pos[j][1] * y)) + p = mat * p + verts[i * vps + j] = p + v1 + if i: + for j in range(fps): + f = (i - 1) * fps + j + faces[f] = [base + j, base + vps + j, + base + vps + (j + 1) % vps, base + (j + 1) % vps] + base = len(verts) - vps + i = n + for j in range(fps): + f = (i - 1) * fps + j + faces[f] = [base + j, j, (j + 1) % vps, base + (j + 1) % vps] + + return verts, faces + + +# Project a point along a vector onto a plane. +# +# Really, just find the intersection of the line represented by @a point +# and @a dir with the plane represented by @a norm and @a p. However, if +# the point is on or in front of the plane, or the line is parallel to +# the plane, the original point will be returned. +# +# @param point The point to be projected onto the plane. +# @param dir The vector along which the point will be projected. +# @param norm The normal of the plane onto which the point will be +# projected. +# @param p A point through which the plane passes. +# @return A vector representing the projected point, or the +# original point. + +def project_point(point, dir, norm, p): + d = (point - p).dot(norm) + if d >= 0: + # the point is already on or in front of the plane + return point + v = dir.dot(norm) + if v * v < 1e-8: + # the plane is unreachable + return point + return point - dir * d / v + + +# Make a simple strut for debugging. +# +# The strut is just a single quad representing the Z axis of the edge. +# +# @param mesh The base mesh. Used for finding the edge vertices. +# @param edge_num The number of the current edge. For the face vertex +# indices. +# @param edge The edge for which the strut will be built. +# @param od Twice the width of the strut. +# @return A tuple containing a list of vertices and a list of faces. +# The face vertex indices are pre-adjusted by the edge +# number. +# @fixme The face vertex indices should be accurate for the local +# vertices (consistency) + +def make_debug_strut(mesh, edge_num, edge, od): + v = [mesh.verts[edge.verts[0].index].co, + mesh.verts[edge.verts[1].index].co, + None, None] + v[2] = v[1] + edge.z * od / 2 + v[3] = v[0] + edge.z * od / 2 + f = [[edge_num * 4 + 0, edge_num * 4 + 1, + edge_num * 4 + 2, edge_num * 4 + 3]] + return v, f + + +# Make a cylinder with ends clipped to the end-planes of the edge. +# +# The strut is just a single quad representing the Z axis of the edge. +# +# @param mesh The base mesh. Used for finding the edge vertices. +# @param edge_num The number of the current edge. For the face vertex +# indices. +# @param edge The edge for which the strut will be built. +# @param od The diameter of the strut. +# @return A tuple containing a list of vertices and a list of faces. +# The face vertex indices are pre-adjusted by the edge +# number. +# @fixme The face vertex indices should be accurate for the local +# vertices (consistency) + +def make_clipped_cylinder(mesh, edge_num, edge, od): + n = len(cossin) + cyl = [None] * n + v0 = mesh.verts[edge.verts[0].index].co + c0 = v0 + od * edge.y + v1 = mesh.verts[edge.verts[1].index].co + c1 = v1 - od * edge.y + for i in range(n): + x = cossin[i][0] + y = cossin[i][1] + r = (edge.z * x - edge.x * y) * od / 2 + cyl[i] = [c0 + r, c1 + r] + for p in edge.verts[0].planes: + cyl[i][0] = project_point(cyl[i][0], edge.y, p, v0) + for p in edge.verts[1].planes: + cyl[i][1] = project_point(cyl[i][1], -edge.y, p, v1) + v = [None] * n * 2 + f = [None] * n + base = edge_num * n * 2 + for i in range(n): + v[i * 2 + 0] = cyl[i][1] + v[i * 2 + 1] = cyl[i][0] + f[i] = [None] * 4 + f[i][0] = base + i * 2 + 0 + f[i][1] = base + i * 2 + 1 + f[i][2] = base + (i * 2 + 3) % (n * 2) + f[i][3] = base + (i * 2 + 2) % (n * 2) + return v, f + + +# Represent a vertex in the base mesh, with additional information. +# +# These vertices are @b not shared between edges. +# +# @var index The index of the vert in the base mesh +# @var edge The edge to which this vertex is attached. +# @var edges A tuple of indicess of edges attached to this vert, not +# including the edge to which this vertex is attached. +# @var planes List of vectors representing the normals of the planes that +# bisect the angle between this vert's edge and each other +# adjacant edge. + +class SVert: + # Create a vertex holding additional information about the bmesh vertex. + # @param bmvert The bmesh vertex for which additional information is + # to be stored. + # @param bmedge The edge to which this vertex is attached. + + def __init__(self, bmvert, bmedge, edge): + self.index = bmvert.index + self.edge = edge + edges = bmvert.link_edges[:] + edges.remove(bmedge) + self.edges = tuple(map(lambda e: e.index, edges)) + self.planes = [] + + def calc_planes(self, edges): + for ed in self.edges: + self.planes.append(calc_plane_normal(self.edge, edges[ed])) + + +# Represent an edge in the base mesh, with additional information. +# +# Edges do not share vertices so that the edge is always on the front (back? +# must verify) side of all the planes attached to its vertices. If the +# vertices were shared, the edge could be on either side of the planes, and +# there would be planes attached to the vertex that are irrelevant to the +# edge. +# +# @var index The index of the edge in the base mesh. +# @var bmedge Cached reference to this edge's bmedge +# @var verts A tuple of 2 SVert vertices, one for each end of the +# edge. The vertices are @b not shared between edges. +# However, if two edges are connected via a vertex in the +# bmesh, their corresponding SVert vertices will have the +# the same index value. +# @var x The x axis of the edges local frame of reference. +# Initially invalid. +# @var y The y axis of the edges local frame of reference. +# Initialized such that the edge runs from verts[0] to +# verts[1] along the negative y axis. +# @var z The z axis of the edges local frame of reference. +# Initially invalid. + + +class SEdge: + + def __init__(self, bmesh, bmedge): + + self.index = bmedge.index + self.bmedge = bmedge + bmesh.verts.ensure_lookup_table() + self.verts = (SVert(bmedge.verts[0], bmedge, self), + SVert(bmedge.verts[1], bmedge, self)) + self.y = (bmesh.verts[self.verts[0].index].co - + bmesh.verts[self.verts[1].index].co) + self.y.normalize() + self.x = self.z = None + + def set_frame(self, up): + self.x = self.y.cross(up) + self.x.normalize() + self.z = self.x.cross(self.y) + + def calc_frame(self, base_edge): + baxis = base_edge.y + if (self.verts[0].index == base_edge.verts[0].index or + self.verts[1].index == base_edge.verts[1].index): + axis = -self.y + elif (self.verts[0].index == base_edge.verts[1].index or + self.verts[1].index == base_edge.verts[0].index): + axis = self.y + else: + raise ValueError("edges not connected") + if baxis.dot(axis) in (-1, 1): + # aligned axis have their up/z aligned + up = base_edge.z + else: + # Get the unit vector dividing the angle (theta) between baxis and + # axis in two equal parts + h = (baxis + axis) + h.normalize() + # (cos(theta/2), sin(theta/2) * n) where n is the unit vector of the + # axis rotating baxis onto axis + q = Quaternion([baxis.dot(h)] + list(baxis.cross(h))) + # rotate the base edge's up around the rotation axis (blender + # quaternion shortcut:) + up = q * base_edge.z + self.set_frame(up) + + def calc_vert_planes(self, edges): + for v in self.verts: + v.calc_planes(edges) + + def bisect_faces(self): + n1 = self.bmedge.link_faces[0].normal + if len(self.bmedge.link_faces) > 1: + n2 = self.bmedge.link_faces[1].normal + return (n1 + n2).normalized() + return n1 + + def calc_simple_frame(self): + return self.y.cross(select_up(self.y)).normalized() + + def find_edge_frame(self, sedges): + if self.bmedge.link_faces: + return self.bisect_faces() + if self.verts[0].edges or self.verts[1].edges: + edges = list(self.verts[0].edges + self.verts[1].edges) + for i in range(len(edges)): + edges[i] = sedges[edges[i]] + while edges and edges[-1].y.cross(self.y).length < 1e-3: + edges.pop() + if not edges: + return self.calc_simple_frame() + n1 = edges[-1].y.cross(self.y).normalized() + edges.pop() + while edges and edges[-1].y.cross(self.y).cross(n1).length < 1e-3: + edges.pop() + if not edges: + return n1 + n2 = edges[-1].y.cross(self.y).normalized() + return (n1 + n2).normalized() + return self.calc_simple_frame() + + +def calc_plane_normal(edge1, edge2): + if edge1.verts[0].index == edge2.verts[0].index: + axis1 = -edge1.y + axis2 = edge2.y + elif edge1.verts[1].index == edge2.verts[1].index: + axis1 = edge1.y + axis2 = -edge2.y + elif edge1.verts[0].index == edge2.verts[1].index: + axis1 = -edge1.y + axis2 = -edge2.y + elif edge1.verts[1].index == edge2.verts[0].index: + axis1 = edge1.y + axis2 = edge2.y + else: + raise ValueError("edges not connected") + # Both axis1 and axis2 are unit vectors, so this will produce a vector + # bisects the two, so long as they are not 180 degrees apart (in which + # there are infinite solutions). + return (axis1 + axis2).normalized() + + +def build_edge_frames(edges): + edge_set = set(edges) + while edge_set: + edge_queue = [edge_set.pop()] + edge_queue[0].set_frame(edge_queue[0].find_edge_frame(edges)) + while edge_queue: + current_edge = edge_queue.pop() + for i in (0, 1): + for e in current_edge.verts[i].edges: + edge = edges[e] + if edge.x is not None: # edge already processed + continue + edge_set.remove(edge) + edge_queue.append(edge) + edge.calc_frame(current_edge) + + +def make_manifold_struts(truss_obj, od, segments): + bpy.context.scene.objects.active = truss_obj + bpy.ops.object.editmode_toggle() + truss_mesh = bmesh.from_edit_mesh(truss_obj.data).copy() + bpy.ops.object.editmode_toggle() + edges = [None] * len(truss_mesh.edges) + for i, e in enumerate(truss_mesh.edges): + edges[i] = SEdge(truss_mesh, e) + build_edge_frames(edges) + verts = [] + faces = [] + for e, edge in enumerate(edges): + # v, f = make_debug_strut(truss_mesh, e, edge, od) + edge.calc_vert_planes(edges) + v, f = make_clipped_cylinder(truss_mesh, e, edge, od) + verts += v + faces += f + return verts, faces + + +def make_simple_struts(truss_mesh, ind, od, segments, solid, loops): + vps = 2 + if solid: + vps *= 2 + if loops: + vps *= 2 + fps = vps + if not solid: + fps -= 1 + + verts = [None] * len(truss_mesh.edges) * segments * vps + faces = [None] * len(truss_mesh.edges) * segments * fps + vbase = 0 + fbase = 0 + + for e in truss_mesh.edges: + v1 = truss_mesh.vertices[e.vertices[0]] + v2 = truss_mesh.vertices[e.vertices[1]] + v, f = make_strut(v1.co, v2.co, ind, od, segments, solid, loops) + for fv in f: + for i in range(len(fv)): + fv[i] += vbase + for i in range(len(v)): + verts[vbase + i] = v[i] + for i in range(len(f)): + faces[fbase + i] = f[i] + # if not base % 12800: + # print (base * 100 / len(verts)) + vbase += vps * segments + fbase += fps * segments + + return verts, faces + + +def create_struts(self, context, ind, od, segments, solid, loops, manifold): + build_cossin(segments) + + for truss_obj in bpy.context.scene.objects: + if not truss_obj.select: + continue + truss_obj.select = False + truss_mesh = truss_obj.to_mesh(context.scene, True, 'PREVIEW') + if not truss_mesh.edges: + continue + if manifold: + verts, faces = make_manifold_struts(truss_obj, od, segments) + else: + verts, faces = make_simple_struts(truss_mesh, ind, od, segments, + solid, loops) + mesh = bpy.data.meshes.new("Struts") + mesh.from_pydata(verts, [], faces) + obj = bpy.data.objects.new("Struts", mesh) + bpy.context.scene.objects.link(obj) + obj.select = True + obj.location = truss_obj.location + bpy.context.scene.objects.active = obj + mesh.update() + + +class Struts(Operator): + bl_idname = "mesh.generate_struts" + bl_label = "Struts" + bl_description = ("Add one or more struts meshes based on selected truss meshes \n" + "Note: can get very high poly\n" + "Needs an existing Active Mesh Object") + bl_options = {'REGISTER', 'UNDO'} + + ind = FloatProperty( + name="Inside Diameter", + description="Diameter of inner surface", + min=0.0, soft_min=0.0, + max=100, soft_max=100, + default=0.04 + ) + od = FloatProperty( + name="Outside Diameter", + description="Diameter of outer surface", + min=0.001, soft_min=0.001, + max=100, soft_max=100, + default=0.05 + ) + manifold = BoolProperty( + name="Manifold", + description="Connect struts to form a single solid", + default=False + ) + solid = BoolProperty( + name="Solid", + description="Create inner surface", + default=False + ) + loops = BoolProperty( + name="Loops", + description="Create sub-surf friendly loops", + default=False + ) + segments = IntProperty( + name="Segments", + description="Number of segments around strut", + min=3, soft_min=3, + max=64, soft_max=64, + default=12 + ) + + def draw(self, context): + layout = self.layout + + col = layout.column(align=True) + col.prop(self, "ind") + col.prop(self, "od") + col.prop(self, "segments") + col.separator() + + col.prop(self, "manifold") + col.prop(self, "solid") + col.prop(self, "loops") + + @classmethod + def poll(cls, context): + obj = context.active_object + return obj is not None and obj.type == "MESH" + + def execute(self, context): + store_undo = bpy.context.user_preferences.edit.use_global_undo + bpy.context.user_preferences.edit.use_global_undo = False + keywords = self.as_keywords() + + try: + create_struts(self, context, **keywords) + bpy.context.user_preferences.edit.use_global_undo = store_undo + + return {"FINISHED"} + + except Exception as e: + bpy.context.user_preferences.edit.use_global_undo = store_undo + self.report({"WARNING"}, + "Make Struts could not be performed. Operation Cancelled") + print("\n[mesh.generate_struts]\n{}".format(e)) + return {"CANCELLED"} + + +def register(): + bpy.utils.register_module(__name__) + + +def unregister(): + bpy.utils.unregister_module(__name__) + + +if __name__ == "__main__": + register() |