Skip to content
Snippets Groups Projects
add_mesh_icicle_gen.py 13.5 KiB
Newer Older
  • Learn to ignore specific revisions
  • bl_info = {
        "name": "Icicle Generator",
        "author": "Eoin Brennan (Mayeoin Bread)",
        "version": (2, 2, 1),
    
        "blender": (2, 74, 0),
    
        "location": "View3D > Add > Mesh",
        "description": "Construct a linear string of icicles of different sizes",
        "warning": "",
        "wiki_url": "",
        "tracker_url": "",
        "category": "Add Mesh"
        }
    
    
    import bpy
    import bmesh
    from mathutils import Vector
    
    from math import (
            pi, sin,
            cos, atan,
            )
    from bpy.props import (
            EnumProperty,
            FloatProperty,
            IntProperty,
            )
    
    import random
    
    
    class IcicleGenerator(bpy.types.Operator):
        bl_idname = "mesh.icicle_gen"
        bl_label = "Icicle Generator"
    
        bl_description = ("Create Icicles on selected Edges of an existing Mesh Object\n"
                          "Note: doesn't work with vertical Edges")
    
        bl_options = {"REGISTER", "UNDO"}
    
        # Maximum radius
    
        maxR: FloatProperty(
    
                name="Max",
                description="Maximum radius of a cone",
                default=0.15,
                min=0.01,
                max=1.0,
                unit="LENGTH"
                )
    
        minR: FloatProperty(
    
                name="Min",
                description="Minimum radius of a cone",
                default=0.025,
                min=0.01,
                max=1.0,
                unit="LENGTH"
                )
    
        maxD: FloatProperty(
    
                name="Max",
                description="Maximum depth (height) of cone",
                default=2.0,
                min=0.2,
                max=2.0,
                unit="LENGTH"
                )
    
        minD: FloatProperty(
    
                name="Min",
                description="Minimum depth (height) of cone",
                default=1.5,
                min=0.2,
                max=2.0,
                unit="LENGTH"
                )
    
        # Number of verts at base of cone
    
        verts: IntProperty(
    
                name="Vertices",
                description="Number of vertices at the icicle base",
                default=8,
                min=3,
                max=24
                )
    
        addCap: EnumProperty(
    
                name="Fill cap",
                description="Fill the icicle cone base",
                items=[
                    ('NGON', "Ngon", "Fill with Ngons"),
                    ('NOTHING', "None", "Do not fill"),
                    ('TRIFAN', "Triangle fan", "Fill with triangles")
                ],
                default='NGON',
                )
    
        # Number of iterations before giving up trying to add cones
        # Prevents crashes and freezes
        # Obviously, the more iterations, the more time spent calculating.
        # Max value (10,000) is safe but can be slow,
        # 2000 to 5000 should be adequate for 95% of cases
    
                name="Iterations",
                description="Number of iterations before giving up, prevents freezing/crashing",
                default=2000,
                min=1,
                max=10000
                )
        verticalEdges = False
    
        @classmethod
        def poll(cls, context):
            obj = context.active_object
            return obj and obj.type == "MESH"
    
        def draw(self, context):
            layout = self.layout
            col = layout.column(align=True)
    
    
            col.label(text="Radius:")
    
            col.prop(self, "minR")
            col.prop(self, "maxR")
    
    
            col.label(text="Depth:")
    
            col.prop(self, "minD")
            col.prop(self, "maxD")
    
    
            col.label(text="Base:")
    
            col.prop(self, "verts")
            col.prop(self, "addCap", text="")
    
            layout.prop(self, "its")
    
            # Variables
            if self.minR > self.maxR:
                self.maxR = self.minR
    
            if self.minD > self.maxD:
                self.maxD = self.minD
    
    
            rad = self.maxR
            radM = self.minR
            depth = self.maxD
            minD = self.minD
    
            addCap = self.addCap
            self.verticalEdges = False
    
            # --- Nested utility functions START --- #
    
            def test_data(obj):
                me = obj.data
                is_edges = bool(len(me.edges) > 0)
                is_selected = False
    
                for edge in me.edges:
                    if edge.select:
                        is_selected = True
                        break
    
                return (is_edges and is_selected)
    
            def flip_to_edit_mode():
                bpy.ops.object.mode_set(mode='OBJECT')
                bpy.ops.object.mode_set(mode='EDIT')
    
            def add_cone(x, y, z, randrad, rd, fill="NGON"):
                bpy.ops.mesh.primitive_cone_add(
                        vertices=self.verts,
                        radius1=randrad,
                        radius2=0.0,
                        depth=rd,
                        end_fill_type=fill,
                        view_align=False,
                        location=(x, y, z),
                        rotation=(pi, 0.0, 0.0)
                        )
    
            # Add icicle function
    
            def add_icicles(rad, radM, depth, minD):
                pos1 = Vector((0.0, 0.0, 0.0))
                pos2 = Vector((0.0, 0.0, 0.0))
                pos = 0
    
                obj = bpy.context.active_object
    
                bm = bmesh.from_edit_mesh(obj.data)
                wm = obj.matrix_world
    
                # Vectors for selected verts
                for v in bm.verts:
                    if v.select:
                        if pos == 0:
                            p1 = v.co
                            pos = 1
                        elif pos == 1:
                            p2 = v.co
                            pos = 2
    
                # Set first to left most vert on X-axis...
                if(p1.x > p2.x):
                    pos1 = p2
                    pos2 = p1
                # Or bottom-most on Y-axis if X-axis not used
                elif(p1.x == p2.x):
                    if(p1.y > p2.y):
                        pos1 = p2
                        pos2 = p1
                    else:
                        pos1 = p1
                        pos2 = p2
                else:
                    pos1 = p1
                    pos2 = p2
                # World matrix for positioning
                pos1 = pos1 * wm
                pos2 = pos2 * wm
    
                # X values not equal, working on X-Y-Z planes
                if pos1.x != pos2.x:
                    # Get the angle of the line
    
                    if (pos2.y != pos1.y):
    
                        angle = atan((pos2.x - pos1.x) / (pos2.y - pos1.y))
                    else:
                        angle = pi / 2
                    # Total length of line, neglect Z-value (Z only affects height)
                    xLength = (((pos2.x - pos1.x)**2) + ((pos2.y - pos1.y)**2))**0.5
                    # Slopes if lines
                    zSlope = (pos2.z - pos1.z) / (pos2.x - pos1.x)
    
                    # Fixes positioning error with some angles
                    if (angle < 0):
                        i = pos2.x
                        j = pos2.y
                        k = pos2.z
                    else:
                        i = pos1.x
                        j = pos1.y
                        k = pos1.z
                    l = 0.0
    
    
                    # if equal values, radius should be that size otherwise randomize it
                    randrad = rad if (radM == rad) else (rad - radM) * random.random()
    
    
                    # Depth, as with radius above
    
                    rd = depth if (depth == minD) else (depth - minD) * random.random()
    
    
                    # Get user iterations
                    iterations = self.its
                    # Counter for iterations
                    c = 0
    
                    while (l < xLength) and (c < iterations):
                        rr = randrad if (radM == rad) else randrad + radM
                        dd = rd if (depth == minD) else rd + minD
    
    
                        # Icicles generally taller than wider, check if true
    
                            # If the new icicle won't exceed line length
                            # Fix for overshooting lines
    
                            if (l + rr + rr <= xLength):
    
                                # Using sine/cosine of angle keeps icicles consistently spaced
                                i = i + (rr) * sin(angle)
                                j = j + (rr) * cos(angle)
                                l = l + rr
    
                                # Add a cone in new position
    
                                add_cone(i, j, (i * zSlope) + (zInt - (dd) / 2), rr, dd, addCap)
    
                                # Add another radius to i and j to prevent overlap
    
                                i = i + (rr) * sin(angle)
                                j = j + (rr) * cos(angle)
                                l = l + rr
    
                                # New values for rad and depth
    
                                randrad = rad if (radM == rad) else (rad - radM) * random.random()
                                rd = depth if (depth == minD) else (depth - minD) * random.random()
    
    
                            # If overshoot, try find smaller cone
                            else:
    
                                randrad = rad if (radM == rad) else (rad - radM) * random.random()
                                rd = depth if (depth == minD) else (depth - minD) * random.random()
    
    
                        # If wider than taller, try find taller than wider
                        else:
    
                            randrad = rad if (radM == rad) else (rad - radM) * random.random()
                            rd = depth if (depth == minD) else (depth - minD) * random.random()
    
    
                        # Increase iterations by 1
                        c = c + 1
    
                # If X values equal, then just working in Y-Z plane,
                # Provided Y values not equal
                elif (pos1.x == pos2.x) and (pos1.y != pos2.y):
                    # Absolute length of Y line
                    xLength = ((pos2.y - pos1.y)**2)**0.5
                    i = pos1.x
                    j = pos1.y
                    k = pos1.z
                    l = 0.0
                    # Z-slope and intercept
                    zSlope = (pos2.z - pos1.z) / (pos2.y - pos1.y)
                    zInt = k - (zSlope * j)
    
                    # Same as above for X-Y-Z plane, just X values don't change
    
                    randrad = rad if (radM == rad) else (rad - radM) * random.random()
                    rd = depth if (depth == minD) else (depth - minD) * random.random()
    
    
                    iterations = self.its
                    c = 0
                    while(l < xLength) and (c < iterations):
    
                        rr = randrad if (radM == rad) else randrad + radM
                        dd = rd if (depth == minD) else rd + minD
    
                        if (dd > rr):
                            if (l + rr + rr <= xLength):
    
    
                                add_cone(i, j, (i * zSlope) + (zInt - (dd) / 2), rr, dd, addCap)
    
    
    
                                randrad = rad if (radM == rad) else (rad - radM) * random.random()
                                rd = depth if (depth == minD) else (depth - minD) * random.random()
    
    
                                randrad = rad if (radM == rad) else (rad - radM) * random.random()
                                rd = depth if (depth == minD) else (depth - minD) * random.random()
    
                            randrad = rad if (radM == rad) else (rad - radM) * random.random()
                            rd = depth if (depth == minD) else (depth - minD) * random.random()
    
    
                    # Otherwise X and Y values the same, so either verts are on top of each other
                    # Or its a vertical line. Either way, we don't like it
                    self.verticalEdges = True
    
            def runIt(rad, radM, depth, minD):
                # Check that min values are less than max values
    
                if not((rad >= radM) and (depth >= minD)):
                    return False
    
                obj = bpy.context.active_object
                if obj.mode == 'EDIT':
                    # List of initial edges
                    oEdge = []
                    bm = bmesh.from_edit_mesh(obj.data)
                    bm.edges.ensure_lookup_table()
    
                    for e in bm.edges:
                        if e.select:
                            # Append selected edges to list
                            oEdge.append(e.index)
    
                    # For every initially selected edge, add cones
                    for e in oEdge:
                        bpy.ops.mesh.select_all(action='DESELECT')
                        bm.edges.ensure_lookup_table()
                        bm.edges[e].select = True
                        add_icicles(rad, radM, depth, minD)
                    return True
    
                return False
    
            # --- Nested utility functions END --- #
    
    
            obj = context.active_object
    
            if obj and obj.type == 'MESH':
                flip_to_edit_mode()
                if not test_data(obj):
                    self.report({'INFO'},
                                "Active Mesh has to have at least one selected Edge. Operation Cancelled")
                    return {"CANCELLED"}
    
                check = runIt(rad, radM, depth, minD)
    
                if check is False:
                    self.report({'INFO'}, "Operation could not be completed")
    
                if self.verticalEdges:
                    self.report({'INFO'},
                                "Some selected vertical Edges were skipped during icicles creation")
    
            return {'FINISHED'}
    
    # Add to menu and register/unregister stuff
    
    def menu_func(self, context):
    
        self.layout.operator(IcicleGenerator.bl_idname, text="Icicle")
    
    
    
    def register():
        bpy.utils.register_class(IcicleGenerator)
    
        bpy.types.VIEW3D_MT_mesh_add.append(menu_func)
    
    
    
    def unregister():
        bpy.utils.unregister_class(IcicleGenerator)
    
        bpy.types.VIEW3D_MT_mesh_add.remove(menu_func)
    
    if __name__ == "__main__":
        register()